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,23 @@
1
+ import type { Config } from "./config.js";
2
+ export type MiddlewareNext<TResult> = () => Promise<TResult> | TResult;
3
+ export type MiddlewareCall<TArgs extends unknown[], TResult> = (...args: [...TArgs, MiddlewareNext<TResult>]) => Promise<TResult> | TResult;
4
+ export type MiddlewareConstructor<TArgs extends unknown[], TResult> = new (...args: unknown[]) => {
5
+ call: MiddlewareCall<TArgs, TResult>;
6
+ config?: Config;
7
+ setConfig?: (config: Config) => void;
8
+ };
9
+ export declare class MiddlewareChain<TArgs extends unknown[], TResult> {
10
+ private readonly config?;
11
+ private entries;
12
+ constructor(config?: Config);
13
+ add(klass: MiddlewareConstructor<TArgs, TResult>, ...args: unknown[]): void;
14
+ use(klass: MiddlewareConstructor<TArgs, TResult>, ...args: unknown[]): void;
15
+ prepend(klass: MiddlewareConstructor<TArgs, TResult>, ...args: unknown[]): void;
16
+ insertBefore(oldklass: MiddlewareConstructor<TArgs, TResult>, newklass: MiddlewareConstructor<TArgs, TResult>, ...args: unknown[]): void;
17
+ insertAfter(oldklass: MiddlewareConstructor<TArgs, TResult>, newklass: MiddlewareConstructor<TArgs, TResult>, ...args: unknown[]): void;
18
+ remove(klass: MiddlewareConstructor<TArgs, TResult>): void;
19
+ exists(klass: MiddlewareConstructor<TArgs, TResult>): boolean;
20
+ clear(): void;
21
+ invoke(...args: [...TArgs, MiddlewareNext<TResult>]): Promise<TResult> | TResult;
22
+ }
23
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,MAAM,cAAc,CAAC,OAAO,IAAI,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;AAEvE,MAAM,MAAM,cAAc,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,IAAI,CAC7D,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,KACzC,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;AAEhC,MAAM,MAAM,qBAAqB,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,IAAI,KACpE,GAAG,IAAI,EAAE,OAAO,EAAE,KACf;IACH,IAAI,EAAE,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,CAAC;AA8BF,qBAAa,eAAe,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,OAAO,CAAyC;gBAE5C,MAAM,CAAC,EAAE,MAAM;IAI3B,GAAG,CAAC,KAAK,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAK3E,GAAG,CAAC,KAAK,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAI3E,OAAO,CACL,KAAK,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAC5C,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,IAAI;IAKP,YAAY,CACV,QAAQ,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAC/C,QAAQ,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAC/C,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,IAAI;IAgBP,WAAW,CACT,QAAQ,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAC/C,QAAQ,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAC/C,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,IAAI;IAgBP,MAAM,CAAC,KAAK,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI;IAI1D,MAAM,CAAC,KAAK,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO;IAI7D,KAAK,IAAI,IAAI;IAIb,MAAM,CACJ,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,GAC3C,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO;CAmB9B"}
@@ -0,0 +1,92 @@
1
+ class MiddlewareEntry {
2
+ config;
3
+ klass;
4
+ args;
5
+ constructor(config, klass, args) {
6
+ this.config = config;
7
+ this.klass = klass;
8
+ this.args = args;
9
+ }
10
+ makeNew() {
11
+ const instance = new this.klass(...this.args);
12
+ if (this.config) {
13
+ if (typeof instance.setConfig === "function") {
14
+ instance.setConfig(this.config);
15
+ }
16
+ else if ("config" in instance) {
17
+ instance.config = this.config;
18
+ }
19
+ }
20
+ return instance;
21
+ }
22
+ }
23
+ export class MiddlewareChain {
24
+ config;
25
+ entries = [];
26
+ constructor(config) {
27
+ this.config = config;
28
+ }
29
+ add(klass, ...args) {
30
+ this.remove(klass);
31
+ this.entries.push(new MiddlewareEntry(this.config, klass, args));
32
+ }
33
+ use(klass, ...args) {
34
+ this.add(klass, ...args);
35
+ }
36
+ prepend(klass, ...args) {
37
+ this.remove(klass);
38
+ this.entries.unshift(new MiddlewareEntry(this.config, klass, args));
39
+ }
40
+ insertBefore(oldklass, newklass, ...args) {
41
+ const existingIndex = this.entries.findIndex((entry) => entry.klass === newklass);
42
+ const entry = existingIndex === -1
43
+ ? new MiddlewareEntry(this.config, newklass, args)
44
+ : this.entries.splice(existingIndex, 1)[0];
45
+ const index = this.entries.findIndex((e) => e.klass === oldklass);
46
+ if (index === -1) {
47
+ this.entries.unshift(entry);
48
+ }
49
+ else {
50
+ this.entries.splice(index, 0, entry);
51
+ }
52
+ }
53
+ insertAfter(oldklass, newklass, ...args) {
54
+ const existingIndex = this.entries.findIndex((entry) => entry.klass === newklass);
55
+ const entry = existingIndex === -1
56
+ ? new MiddlewareEntry(this.config, newklass, args)
57
+ : this.entries.splice(existingIndex, 1)[0];
58
+ const index = this.entries.findIndex((e) => e.klass === oldklass);
59
+ if (index === -1) {
60
+ this.entries.push(entry);
61
+ }
62
+ else {
63
+ this.entries.splice(index + 1, 0, entry);
64
+ }
65
+ }
66
+ remove(klass) {
67
+ this.entries = this.entries.filter((entry) => entry.klass !== klass);
68
+ }
69
+ exists(klass) {
70
+ return this.entries.some((entry) => entry.klass === klass);
71
+ }
72
+ clear() {
73
+ this.entries = [];
74
+ }
75
+ invoke(...args) {
76
+ if (this.entries.length === 0) {
77
+ const last = args.at(-1);
78
+ return last();
79
+ }
80
+ const chain = this.entries.map((entry) => entry.makeNew());
81
+ const callNext = (index) => {
82
+ if (index >= chain.length) {
83
+ const last = args.at(-1);
84
+ return last();
85
+ }
86
+ const middleware = chain[index];
87
+ const next = () => callNext(index + 1);
88
+ return middleware.call(...args.slice(0, -1), next);
89
+ };
90
+ return callNext(0);
91
+ }
92
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Periodic job scheduler for Sidekiq.
3
+ *
4
+ * Allows registering jobs that run on a cron schedule.
5
+ * Only the leader process enqueues periodic jobs to prevent duplicates.
6
+ */
7
+ import type { Config } from "./config.js";
8
+ import type { JobConstructor } from "./job.js";
9
+ import type { LeaderElector } from "./leader.js";
10
+ import type { JobOptions } from "./types.js";
11
+ export interface PeriodicJobOptions extends JobOptions {
12
+ /** Job arguments */
13
+ args?: unknown[];
14
+ }
15
+ export interface PeriodicJobConfig {
16
+ /** Unique identifier for this periodic job */
17
+ lid: string;
18
+ /** Cron expression */
19
+ cron: string;
20
+ /** Job class name */
21
+ class: string;
22
+ /** Target queue */
23
+ queue: string;
24
+ /** Job arguments */
25
+ args: unknown[];
26
+ /** Additional job options */
27
+ options: PeriodicJobOptions;
28
+ }
29
+ export declare class PeriodicScheduler {
30
+ private readonly config;
31
+ private readonly leaderElector;
32
+ private readonly jobs;
33
+ private readonly client;
34
+ private redis?;
35
+ private running;
36
+ private loopPromise?;
37
+ constructor(config: Config, leaderElector: LeaderElector);
38
+ /**
39
+ * Register a periodic job.
40
+ *
41
+ * @param cron - Standard 5-field cron expression
42
+ * @param jobClass - Job class to execute
43
+ * @param options - Additional job options
44
+ * @returns The unique lid for this periodic job
45
+ */
46
+ register(cron: string, jobClass: JobConstructor, options?: PeriodicJobOptions): string;
47
+ /**
48
+ * Unregister a periodic job by its lid.
49
+ */
50
+ unregister(lid: string): boolean;
51
+ /**
52
+ * List all registered periodic jobs.
53
+ */
54
+ list(): PeriodicJobConfig[];
55
+ /**
56
+ * Start the periodic job loop.
57
+ */
58
+ start(): Promise<void>;
59
+ /**
60
+ * Stop the periodic job loop.
61
+ */
62
+ stop(): Promise<void>;
63
+ /**
64
+ * Generate a deterministic lid for a periodic job.
65
+ */
66
+ private generateLid;
67
+ /**
68
+ * Sync registered jobs to Redis for visibility.
69
+ */
70
+ private syncToRedis;
71
+ /**
72
+ * Background loop for periodic job scheduling.
73
+ */
74
+ private periodicLoop;
75
+ /**
76
+ * Enqueue a periodic job if it hasn't been enqueued recently.
77
+ */
78
+ private enqueueIfNotRecent;
79
+ }
80
+ //# sourceMappingURL=periodic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"periodic.d.ts","sourceRoot":"","sources":["../src/periodic.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ7C,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,oBAAoB;IACpB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,6BAA6B;IAC7B,OAAO,EAAE,kBAAkB,CAAC;CAC7B;AAQD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAC,CAAgB;gBAExB,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa;IAMxD;;;;;;;OAOG;IACH,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE,kBAAuB,GAC/B,MAAM;IA2BT;;OAEG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAQhC;;OAEG;IACH,IAAI,IAAI,iBAAiB,EAAE;IAI3B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB3B;;OAEG;IACH,OAAO,CAAC,WAAW;IAiBnB;;OAEG;YACW,WAAW;IAoBzB;;OAEG;YAEW,YAAY;IA2C1B;;OAEG;YACW,kBAAkB;CAmCjC"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Periodic job scheduler for Sidekiq.
3
+ *
4
+ * Allows registering jobs that run on a cron schedule.
5
+ * Only the leader process enqueues periodic jobs to prevent duplicates.
6
+ */
7
+ import { Client } from "./client.js";
8
+ import { parseCron, shouldRunAt } from "./cron.js";
9
+ import { dumpJson } from "./json.js";
10
+ const CRON_KEY = "cron";
11
+ const CRON_LOCK_PREFIX = "cron:lock:";
12
+ const CRON_LOCK_TTL_SECONDS = 60;
13
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14
+ export class PeriodicScheduler {
15
+ config;
16
+ leaderElector;
17
+ jobs = new Map();
18
+ client;
19
+ redis;
20
+ running = false;
21
+ loopPromise;
22
+ constructor(config, leaderElector) {
23
+ this.config = config;
24
+ this.leaderElector = leaderElector;
25
+ this.client = new Client({ config });
26
+ }
27
+ /**
28
+ * Register a periodic job.
29
+ *
30
+ * @param cron - Standard 5-field cron expression
31
+ * @param jobClass - Job class to execute
32
+ * @param options - Additional job options
33
+ * @returns The unique lid for this periodic job
34
+ */
35
+ register(cron, jobClass, options = {}) {
36
+ const schedule = parseCron(cron);
37
+ const queue = options.queue ?? jobClass.getSidekiqOptions().queue ?? "default";
38
+ const args = options.args ?? [];
39
+ // Generate a deterministic lid based on class name and cron
40
+ const lid = this.generateLid(jobClass.name, cron, queue, args);
41
+ const config = {
42
+ lid,
43
+ cron,
44
+ class: jobClass.name,
45
+ queue,
46
+ args,
47
+ options,
48
+ };
49
+ this.jobs.set(lid, { config, schedule, jobClass });
50
+ this.config.logger.debug(() => `Registered periodic job: ${jobClass.name} (${cron})`);
51
+ return lid;
52
+ }
53
+ /**
54
+ * Unregister a periodic job by its lid.
55
+ */
56
+ unregister(lid) {
57
+ const existed = this.jobs.delete(lid);
58
+ if (existed) {
59
+ this.config.logger.debug(() => `Unregistered periodic job: ${lid}`);
60
+ }
61
+ return existed;
62
+ }
63
+ /**
64
+ * List all registered periodic jobs.
65
+ */
66
+ list() {
67
+ return Array.from(this.jobs.values()).map((job) => job.config);
68
+ }
69
+ /**
70
+ * Start the periodic job loop.
71
+ */
72
+ async start() {
73
+ if (this.running) {
74
+ return;
75
+ }
76
+ this.redis = await this.config.getRedisClient();
77
+ this.running = true;
78
+ // Persist job configs to Redis for visibility
79
+ await this.syncToRedis();
80
+ // Start the background loop
81
+ this.loopPromise = this.periodicLoop();
82
+ }
83
+ /**
84
+ * Stop the periodic job loop.
85
+ */
86
+ async stop() {
87
+ if (!this.running) {
88
+ return;
89
+ }
90
+ this.running = false;
91
+ // Remove job configs from Redis
92
+ if (this.redis && this.jobs.size > 0) {
93
+ try {
94
+ const lids = Array.from(this.jobs.keys());
95
+ await this.redis.hDel(CRON_KEY, lids);
96
+ }
97
+ catch {
98
+ // Ignore errors during shutdown
99
+ }
100
+ }
101
+ // Wait for loop to finish
102
+ if (this.loopPromise) {
103
+ await this.loopPromise;
104
+ }
105
+ }
106
+ /**
107
+ * Generate a deterministic lid for a periodic job.
108
+ */
109
+ generateLid(className, cron, queue, args) {
110
+ // Create a simple hash of the job configuration
111
+ const base = `${className}:${cron}:${queue}:${dumpJson(args)}`;
112
+ let hash = 0;
113
+ for (let i = 0; i < base.length; i += 1) {
114
+ const char = base.charCodeAt(i);
115
+ // biome-ignore lint/suspicious/noBitwiseOperators: intentional hash computation
116
+ hash = ((hash << 5) - hash + char) | 0;
117
+ }
118
+ return `${className.toLowerCase()}-${Math.abs(hash).toString(36)}`;
119
+ }
120
+ /**
121
+ * Sync registered jobs to Redis for visibility.
122
+ */
123
+ async syncToRedis() {
124
+ if (!this.redis || this.jobs.size === 0) {
125
+ return;
126
+ }
127
+ const entries = {};
128
+ for (const [lid, job] of this.jobs) {
129
+ entries[lid] = dumpJson(job.config);
130
+ }
131
+ try {
132
+ await this.redis.hSet(CRON_KEY, entries);
133
+ }
134
+ catch (error) {
135
+ this.config.logger.error(() => `Failed to sync periodic jobs to Redis: ${error.message}`);
136
+ }
137
+ }
138
+ /**
139
+ * Background loop for periodic job scheduling.
140
+ */
141
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: scheduler loop requires multiple conditional checks
142
+ async periodicLoop() {
143
+ // Wait for initial sync with jitter to avoid thundering herd
144
+ await sleep(Math.random() * 5000);
145
+ while (this.running) {
146
+ // Wait until the next minute boundary plus jitter
147
+ const now = Date.now();
148
+ const nextMinute = Math.ceil(now / 60_000) * 60_000;
149
+ const jitter = Math.random() * 2000; // 0-2 seconds
150
+ const waitMs = Math.max(0, nextMinute - now + jitter);
151
+ await sleep(waitMs);
152
+ if (!this.running) {
153
+ break;
154
+ }
155
+ // Only leader enqueues jobs
156
+ if (!this.leaderElector.leader()) {
157
+ continue;
158
+ }
159
+ // Check each registered job
160
+ const checkTime = new Date();
161
+ for (const [lid, job] of this.jobs) {
162
+ if (!this.running) {
163
+ break;
164
+ }
165
+ try {
166
+ if (shouldRunAt(job.schedule, checkTime)) {
167
+ await this.enqueueIfNotRecent(lid, job);
168
+ }
169
+ }
170
+ catch (error) {
171
+ this.config.logger.error(() => `Error checking periodic job ${lid}: ${error.message}`);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ /**
177
+ * Enqueue a periodic job if it hasn't been enqueued recently.
178
+ */
179
+ async enqueueIfNotRecent(lid, job) {
180
+ if (!this.redis) {
181
+ return;
182
+ }
183
+ // Try to acquire the lock (prevents duplicate enqueue)
184
+ const lockKey = `${CRON_LOCK_PREFIX}${lid}`;
185
+ const result = await this.redis.set(lockKey, "1", {
186
+ NX: true,
187
+ EX: CRON_LOCK_TTL_SECONDS,
188
+ });
189
+ if (result !== "OK") {
190
+ // Lock already held, job was already enqueued this minute
191
+ return;
192
+ }
193
+ // Enqueue the job
194
+ const { queue, retry, tags, ...restOptions } = job.config.options;
195
+ const jid = await this.client.push({
196
+ class: job.jobClass,
197
+ args: job.config.args,
198
+ queue: job.config.queue,
199
+ retry,
200
+ tags,
201
+ ...restOptions,
202
+ });
203
+ this.config.logger.info(() => `Enqueued periodic job ${job.config.class} (${lid}) with jid ${jid}`);
204
+ }
205
+ }
@@ -0,0 +1,3 @@
1
+ import type { createClient } from "redis";
2
+ export type RedisClient = ReturnType<typeof createClient>;
3
+ //# sourceMappingURL=redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAE1C,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC"}
package/dist/redis.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export type RegisteredJobClass = new () => {
2
+ perform: (...args: any[]) => Promise<void> | void;
3
+ jid?: string;
4
+ _context?: {
5
+ stopping: () => boolean;
6
+ };
7
+ };
8
+ export declare const registerJob: (klass: RegisteredJobClass) => void;
9
+ export declare const resolveJob: (name: string) => RegisteredJobClass | undefined;
10
+ export declare const registeredJob: (name: string) => RegisteredJobClass | undefined;
11
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,UAAU;IAEzC,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;KAAE,CAAC;CACxC,CAAC;AAIF,eAAO,MAAM,WAAW,GAAI,OAAO,kBAAkB,KAAG,IAIvD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,kBAAkB,GAAG,SAC3C,CAAC;AAErB,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,kBAAkB,GAAG,SAC9C,CAAC"}
@@ -0,0 +1,8 @@
1
+ const registry = new Map();
2
+ export const registerJob = (klass) => {
3
+ if (klass?.name) {
4
+ registry.set(klass.name, klass);
5
+ }
6
+ };
7
+ export const resolveJob = (name) => registry.get(name);
8
+ export const registeredJob = (name) => registry.get(name);
@@ -0,0 +1,81 @@
1
+ import type { Config } from "./config.js";
2
+ import { PeriodicScheduler } from "./periodic.js";
3
+ import type { JobPayload } from "./types.js";
4
+ export interface WorkSnapshot {
5
+ workerId: string;
6
+ queue: string;
7
+ payloadRaw: string;
8
+ payload?: JobPayload;
9
+ runAt: number;
10
+ elapsed: number;
11
+ }
12
+ export declare class Runner {
13
+ private readonly config;
14
+ private quieting;
15
+ private stopping;
16
+ private readonly workers;
17
+ private schedulerHandle?;
18
+ private schedulerRunning;
19
+ private heartbeatHandle?;
20
+ private readonly queueStrategy;
21
+ private baseRedis?;
22
+ private readonly workerRedis;
23
+ private readonly identity;
24
+ private readonly startedAt;
25
+ private readonly workState;
26
+ private readonly inProgress;
27
+ private lastCleanupAt;
28
+ private readonly rttReadings;
29
+ private readonly jobLogger;
30
+ private leaderElector?;
31
+ private _periodicScheduler?;
32
+ constructor(config: Config);
33
+ start(): Promise<void>;
34
+ quiet(): Promise<void>;
35
+ stop(): Promise<void>;
36
+ /**
37
+ * Returns true if this process is currently the leader.
38
+ */
39
+ leader(): boolean;
40
+ /**
41
+ * Get the periodic scheduler for registering cron jobs.
42
+ */
43
+ get periodicScheduler(): PeriodicScheduler | undefined;
44
+ snapshotWork(): WorkSnapshot[];
45
+ private startScheduler;
46
+ private runSchedulerLoop;
47
+ private initialWait;
48
+ private processCount;
49
+ private scaledPollInterval;
50
+ private randomPollInterval;
51
+ private stopScheduler;
52
+ private heartbeat;
53
+ private handleSignal;
54
+ private dumpWorkState;
55
+ private clearHeartbeat;
56
+ private processInfo;
57
+ private checkRtt;
58
+ private recordRtt;
59
+ private waitForDrain;
60
+ private waitForWorkers;
61
+ private requeueInProgress;
62
+ private sendRawToMorgue;
63
+ private runWithProfiler;
64
+ private cleanupProcesses;
65
+ private startHeartbeat;
66
+ private stopHeartbeat;
67
+ private enqueueScheduled;
68
+ private workLoop;
69
+ private fetchWork;
70
+ private processJob;
71
+ private handleFailure;
72
+ private safeRetryIn;
73
+ private retriesExhausted;
74
+ private sendToMorgue;
75
+ private runDeathHandlers;
76
+ private runErrorHandlers;
77
+ private buildErrorContext;
78
+ private safeErrorMessage;
79
+ private updateStat;
80
+ }
81
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAU1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAqG7C,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,eAAe,CAAC,CAAiB;IACzC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,eAAe,CAAC,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,SAAS,CAAC,CAAgD;IAClE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAEnB;IACT,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAGtB;IACJ,OAAO,CAAC,QAAQ,CAAC,UAAU,CAGvB;IACJ,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,kBAAkB,CAAC,CAAoB;gBAEnC,MAAM,EAAE,MAAM;IAQpB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B3B;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;OAEG;IACH,IAAI,iBAAiB,IAAI,iBAAiB,GAAG,SAAS,CAErD;IAED,YAAY,IAAI,YAAY,EAAE;IAsB9B,OAAO,CAAC,cAAc;YAKR,gBAAgB;YAWhB,WAAW;YAoBX,YAAY;IAM1B,OAAO,CAAC,kBAAkB;YAIZ,kBAAkB;IAqBhC,OAAO,CAAC,aAAa;YAQP,SAAS;YA0DT,YAAY;IAoB1B,OAAO,CAAC,aAAa;YAkBP,cAAc;IAa5B,OAAO,CAAC,WAAW;YAcL,QAAQ;IAWtB,OAAO,CAAC,SAAS;YAoBH,YAAY;YAMZ,cAAc;YAQd,iBAAiB;YAkBjB,eAAe;YAWf,eAAe;YAWf,gBAAgB;IA8B9B,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,aAAa;YAOP,gBAAgB;YA6BhB,QAAQ;YAiCR,SAAS;YA4BT,UAAU;YAwEV,aAAa;IA2H3B,OAAO,CAAC,WAAW;YAoBL,gBAAgB;YA8BhB,YAAY;YAYZ,gBAAgB;YAmBhB,gBAAgB;IAsB9B,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,gBAAgB;YAQV,UAAU;CAczB"}