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
package/dist/job.js ADDED
@@ -0,0 +1,200 @@
1
+ import { Client } from "./client.js";
2
+ import { normalizeItem, verifyJson } from "./job-util.js";
3
+ import { Sidekiq } from "./sidekiq.js";
4
+ import { EmptyQueueError, Queues } from "./testing.js";
5
+ const OPTIONS_KEY = Symbol("sidekiqOptions");
6
+ export class Job {
7
+ jid;
8
+ _context;
9
+ static getSidekiqOptions() {
10
+ const base = Sidekiq.defaultJobOptions();
11
+ const explicit = this.sidekiqOptions ?? {};
12
+ const stored = this[OPTIONS_KEY] ?? {};
13
+ return {
14
+ ...base,
15
+ ...explicit,
16
+ ...stored,
17
+ };
18
+ }
19
+ static setSidekiqOptions(options) {
20
+ const stored = this[OPTIONS_KEY] ?? {};
21
+ this[OPTIONS_KEY] = {
22
+ ...stored,
23
+ ...options,
24
+ };
25
+ }
26
+ static queueAs(queue) {
27
+ this.setSidekiqOptions({ queue });
28
+ }
29
+ static retryIn(handler) {
30
+ this.sidekiqRetryIn = handler;
31
+ }
32
+ static retriesExhausted(handler) {
33
+ this.sidekiqRetriesExhausted = handler;
34
+ }
35
+ static set(options) {
36
+ return new JobSetter(this, options);
37
+ }
38
+ static performAsync(...args) {
39
+ return new JobSetter(this, {}).performAsync(...args);
40
+ }
41
+ static performIn(interval, ...args) {
42
+ return new JobSetter(this, {}).performIn(interval, ...args);
43
+ }
44
+ static performAt(timestamp, ...args) {
45
+ return new JobSetter(this, {}).performAt(timestamp, ...args);
46
+ }
47
+ static performBulk(args, options) {
48
+ return new JobSetter(this, {}).performBulk(args, options);
49
+ }
50
+ static performInline(...args) {
51
+ return new JobSetter(this, {}).performInline(...args);
52
+ }
53
+ static jobs() {
54
+ if (this === Job) {
55
+ return Queues.jobs();
56
+ }
57
+ return Queues.jobsForClass(this.name);
58
+ }
59
+ static clear() {
60
+ const queue = this.getSidekiqOptions().queue ?? "default";
61
+ Queues.clearFor(queue, this.name);
62
+ }
63
+ static async drain() {
64
+ while (this.jobs().length > 0) {
65
+ const nextJob = this.jobs()[0];
66
+ Queues.deleteFor(nextJob.jid ?? "", nextJob.queue ?? "default", this.name);
67
+ await this.processJob(nextJob);
68
+ }
69
+ }
70
+ static async performOne() {
71
+ if (this.jobs().length === 0) {
72
+ throw new EmptyQueueError();
73
+ }
74
+ const nextJob = this.jobs()[0];
75
+ Queues.deleteFor(nextJob.jid ?? "", nextJob.queue ?? "default", this.name);
76
+ await this.processJob(nextJob);
77
+ }
78
+ static async processJob(payload) {
79
+ const instance = new this();
80
+ instance.jid = payload.jid;
81
+ instance._context = { stopping: () => false };
82
+ await Sidekiq.defaultConfiguration.serverMiddleware.invoke(instance, payload, payload.queue ?? "default", async () => {
83
+ await instance.perform(...(payload.args ?? []));
84
+ });
85
+ }
86
+ static clearAll() {
87
+ Queues.clearAll();
88
+ }
89
+ static async drainAll() {
90
+ while (Queues.jobs().length > 0) {
91
+ const jobs = Queues.jobs();
92
+ const classes = Array.from(new Set(jobs.map((job) => String(job.class))));
93
+ for (const className of classes) {
94
+ const klass = Sidekiq.registeredJobClass(className);
95
+ if (!klass) {
96
+ continue;
97
+ }
98
+ await klass.drain();
99
+ }
100
+ }
101
+ }
102
+ static clientPush(item) {
103
+ const config = Sidekiq.defaultConfiguration;
104
+ const normalized = normalizeItem(item, Sidekiq.defaultJobOptions());
105
+ verifyJson(normalized.args, config.strictArgs);
106
+ const client = new Client({ config });
107
+ return client.push(normalized);
108
+ }
109
+ logger() {
110
+ return Sidekiq.logger();
111
+ }
112
+ interrupted() {
113
+ return this._context?.stopping() ?? false;
114
+ }
115
+ }
116
+ export class JobSetter {
117
+ klass;
118
+ options;
119
+ constructor(klass, options) {
120
+ this.klass = klass;
121
+ this.options = { ...options };
122
+ const interval = this.options.waitUntil ?? this.options.wait;
123
+ if (interval !== undefined) {
124
+ this.at(interval);
125
+ this.options.wait = undefined;
126
+ this.options.waitUntil = undefined;
127
+ }
128
+ }
129
+ set(options) {
130
+ const merged = { ...options };
131
+ const interval = merged.waitUntil ?? merged.wait;
132
+ if (interval !== undefined) {
133
+ this.at(interval);
134
+ merged.wait = undefined;
135
+ merged.waitUntil = undefined;
136
+ }
137
+ this.options = {
138
+ ...this.options,
139
+ ...merged,
140
+ };
141
+ return this;
142
+ }
143
+ async performAsync(...args) {
144
+ if (this.options.sync === true) {
145
+ return (await this.performInline(...args)) ? "inline" : null;
146
+ }
147
+ const payload = {
148
+ ...this.options,
149
+ class: this.klass,
150
+ args,
151
+ };
152
+ return this.klass.clientPush(payload);
153
+ }
154
+ async performInline(...args) {
155
+ const payload = {
156
+ ...this.options,
157
+ class: this.klass,
158
+ args,
159
+ };
160
+ const normalized = normalizeItem(payload, Sidekiq.defaultJobOptions());
161
+ verifyJson(normalized.args, Sidekiq.defaultConfiguration.strictArgs);
162
+ const instance = new this.klass();
163
+ instance.jid = normalized.jid;
164
+ await instance.perform(...normalized.args);
165
+ return true;
166
+ }
167
+ performBulk(args, options) {
168
+ const payload = {
169
+ ...this.options,
170
+ class: this.klass,
171
+ args: [],
172
+ };
173
+ const client = new Client({ config: Sidekiq.defaultConfiguration });
174
+ return client.pushBulk({
175
+ ...payload,
176
+ args,
177
+ batch_size: options?.batchSize,
178
+ spread_interval: options?.spreadInterval,
179
+ at: options?.at,
180
+ });
181
+ }
182
+ performIn(interval, ...args) {
183
+ return this.at(interval).performAsync(...args);
184
+ }
185
+ performAt(timestamp, ...args) {
186
+ return this.at(timestamp).performAsync(...args);
187
+ }
188
+ at(interval) {
189
+ const int = Number(interval);
190
+ const now = Date.now() / 1000;
191
+ const ts = int < 1_000_000_000 ? now + int : int;
192
+ if (ts > now) {
193
+ this.options.at = ts;
194
+ }
195
+ else {
196
+ this.options.at = undefined;
197
+ }
198
+ return this;
199
+ }
200
+ }
package/dist/json.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const loadJson: (value: string) => unknown;
2
+ export declare const dumpJson: (value: unknown) => string;
3
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,KAAG,OAA4B,CAAC;AAEtE,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,MAA+B,CAAC"}
package/dist/json.js ADDED
@@ -0,0 +1,2 @@
1
+ export const loadJson = (value) => JSON.parse(value);
2
+ export const dumpJson = (value) => JSON.stringify(value);
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Leader election for Sidekiq processes.
3
+ *
4
+ * Follows Sidekiq Enterprise's approach:
5
+ * - Leaders refresh every 15 seconds
6
+ * - Followers check/attempt acquisition every 60 seconds
7
+ * - Redis is the source of truth (no Raft/Paxos)
8
+ * - 20 second TTL on leader key for automatic failover
9
+ */
10
+ import type { Config } from "./config.js";
11
+ export interface LeaderElectorOptions {
12
+ /** Leader refresh interval in milliseconds (default: 15000) */
13
+ refreshInterval?: number;
14
+ /** Follower check interval in milliseconds (default: 60000) */
15
+ checkInterval?: number;
16
+ /** Leader key TTL in seconds (default: 20) */
17
+ ttl?: number;
18
+ /** Override process identity (default: hostname:pid) */
19
+ identity?: string;
20
+ }
21
+ export declare class LeaderElector {
22
+ private readonly config;
23
+ private readonly identity;
24
+ private readonly refreshInterval;
25
+ private readonly checkInterval;
26
+ private readonly ttl;
27
+ private redis?;
28
+ private isLeader;
29
+ private running;
30
+ private loopPromise?;
31
+ private stopController?;
32
+ constructor(config: Config, options?: LeaderElectorOptions);
33
+ /**
34
+ * Start the leader election loop.
35
+ */
36
+ start(): Promise<void>;
37
+ /**
38
+ * Stop the leader election loop and release leadership if held.
39
+ */
40
+ stop(): Promise<void>;
41
+ /**
42
+ * Returns true if this process is currently the leader.
43
+ */
44
+ leader(): boolean;
45
+ /**
46
+ * Get the identity of the current leader (may be another process).
47
+ */
48
+ currentLeader(): Promise<string | null>;
49
+ /**
50
+ * Try to acquire leadership. Returns true if successful.
51
+ */
52
+ private tryAcquire;
53
+ /**
54
+ * Refresh leadership (extend TTL). Returns true if successful.
55
+ */
56
+ private tryRefresh;
57
+ /**
58
+ * Background loop for leader election.
59
+ */
60
+ private leaderLoop;
61
+ private sleepWithAbort;
62
+ }
63
+ //# sourceMappingURL=leader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leader.d.ts","sourceRoot":"","sources":["../src/leader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAe1C,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAC,CAAgB;IACpC,OAAO,CAAC,cAAc,CAAC,CAAkB;gBAE7B,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB;IAS9D;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B3B;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAK7C;;OAEG;YACW,UAAU;IA4BxB;;OAEG;YACW,UAAU;IAmBxB;;OAEG;YACW,UAAU;YAkCV,cAAc;CAkB7B"}
package/dist/leader.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Leader election for Sidekiq processes.
3
+ *
4
+ * Follows Sidekiq Enterprise's approach:
5
+ * - Leaders refresh every 15 seconds
6
+ * - Followers check/attempt acquisition every 60 seconds
7
+ * - Redis is the source of truth (no Raft/Paxos)
8
+ * - 20 second TTL on leader key for automatic failover
9
+ */
10
+ import { hostname } from "node:os";
11
+ const LEADER_KEY = "leader";
12
+ const LEADER_TTL_SECONDS = 20;
13
+ const LEADER_REFRESH_INTERVAL_MS = 15_000;
14
+ const FOLLOWER_CHECK_INTERVAL_MS = 60_000;
15
+ const LUA_REFRESH_LEADER = `
16
+ if redis.call("get", KEYS[1]) == ARGV[1] then
17
+ return redis.call("set", KEYS[1], ARGV[1], "ex", ARGV[2])
18
+ end
19
+ return nil
20
+ `;
21
+ export class LeaderElector {
22
+ config;
23
+ identity;
24
+ refreshInterval;
25
+ checkInterval;
26
+ ttl;
27
+ redis;
28
+ isLeader = false;
29
+ running = false;
30
+ loopPromise;
31
+ stopController;
32
+ constructor(config, options = {}) {
33
+ this.config = config;
34
+ this.identity = options.identity ?? `${hostname()}:${process.pid}`;
35
+ this.refreshInterval =
36
+ options.refreshInterval ?? LEADER_REFRESH_INTERVAL_MS;
37
+ this.checkInterval = options.checkInterval ?? FOLLOWER_CHECK_INTERVAL_MS;
38
+ this.ttl = options.ttl ?? LEADER_TTL_SECONDS;
39
+ }
40
+ /**
41
+ * Start the leader election loop.
42
+ */
43
+ async start() {
44
+ if (this.running) {
45
+ return;
46
+ }
47
+ this.redis = await this.config.getRedisClient();
48
+ this.running = true;
49
+ this.stopController = new AbortController();
50
+ // Try to acquire leadership immediately on start
51
+ await this.tryAcquire();
52
+ // Start the background loop
53
+ this.loopPromise = this.leaderLoop();
54
+ }
55
+ /**
56
+ * Stop the leader election loop and release leadership if held.
57
+ */
58
+ async stop() {
59
+ if (!this.running) {
60
+ return;
61
+ }
62
+ this.running = false;
63
+ this.stopController?.abort();
64
+ // Release leadership on clean shutdown
65
+ if (this.isLeader && this.redis) {
66
+ try {
67
+ const current = await this.redis.get(LEADER_KEY);
68
+ if (current === this.identity) {
69
+ await this.redis.del(LEADER_KEY);
70
+ }
71
+ }
72
+ catch {
73
+ // Ignore errors during shutdown
74
+ }
75
+ }
76
+ this.isLeader = false;
77
+ // Wait for loop to finish
78
+ if (this.loopPromise) {
79
+ await this.loopPromise;
80
+ }
81
+ }
82
+ /**
83
+ * Returns true if this process is currently the leader.
84
+ */
85
+ leader() {
86
+ return this.isLeader;
87
+ }
88
+ /**
89
+ * Get the identity of the current leader (may be another process).
90
+ */
91
+ async currentLeader() {
92
+ const redis = this.redis ?? (await this.config.getRedisClient());
93
+ return redis.get(LEADER_KEY);
94
+ }
95
+ /**
96
+ * Try to acquire leadership. Returns true if successful.
97
+ */
98
+ async tryAcquire() {
99
+ if (!this.redis) {
100
+ return false;
101
+ }
102
+ try {
103
+ const result = await this.redis.set(LEADER_KEY, this.identity, {
104
+ NX: true,
105
+ EX: this.ttl,
106
+ });
107
+ if (result === "OK") {
108
+ if (!this.isLeader) {
109
+ this.isLeader = true;
110
+ await this.config.fireEvent("leader", { oneshot: false });
111
+ this.config.logger.info(() => `Became leader: ${this.identity}`);
112
+ }
113
+ return true;
114
+ }
115
+ }
116
+ catch (error) {
117
+ this.config.logger.error(() => `Leader acquisition error: ${error.message}`);
118
+ }
119
+ return false;
120
+ }
121
+ /**
122
+ * Refresh leadership (extend TTL). Returns true if successful.
123
+ */
124
+ async tryRefresh() {
125
+ if (!this.redis) {
126
+ return false;
127
+ }
128
+ try {
129
+ const result = await this.redis.eval(LUA_REFRESH_LEADER, {
130
+ keys: [LEADER_KEY],
131
+ arguments: [this.identity, String(this.ttl)],
132
+ });
133
+ return result === "OK";
134
+ }
135
+ catch (error) {
136
+ this.config.logger.error(() => `Leader refresh error: ${error.message}`);
137
+ return false;
138
+ }
139
+ }
140
+ /**
141
+ * Background loop for leader election.
142
+ */
143
+ async leaderLoop() {
144
+ while (this.running) {
145
+ try {
146
+ if (this.isLeader) {
147
+ // Leader: try to refresh
148
+ const refreshed = await this.tryRefresh();
149
+ if (!refreshed) {
150
+ this.isLeader = false;
151
+ await this.config.fireEvent("follower", { oneshot: false });
152
+ this.config.logger.info(() => `Lost leadership: ${this.identity}`);
153
+ }
154
+ }
155
+ else {
156
+ // Follower: try to acquire
157
+ await this.tryAcquire();
158
+ }
159
+ }
160
+ catch (error) {
161
+ // On unexpected errors, assume we lost leadership
162
+ if (this.isLeader) {
163
+ this.isLeader = false;
164
+ await this.config.fireEvent("follower", { oneshot: false });
165
+ this.config.logger.warn(() => `Lost leadership due to error: ${error.message}`);
166
+ }
167
+ }
168
+ // Sleep for appropriate interval
169
+ const interval = this.isLeader
170
+ ? this.refreshInterval
171
+ : this.checkInterval;
172
+ await this.sleepWithAbort(interval);
173
+ }
174
+ }
175
+ async sleepWithAbort(ms) {
176
+ const controller = this.stopController;
177
+ if (!controller || controller.signal.aborted) {
178
+ return;
179
+ }
180
+ await new Promise((resolve) => {
181
+ const timeout = setTimeout(() => {
182
+ controller.signal.removeEventListener("abort", onAbort);
183
+ resolve();
184
+ }, ms);
185
+ const onAbort = () => {
186
+ clearTimeout(timeout);
187
+ controller.signal.removeEventListener("abort", onAbort);
188
+ resolve();
189
+ };
190
+ controller.signal.addEventListener("abort", onAbort, { once: true });
191
+ });
192
+ }
193
+ }
@@ -0,0 +1,53 @@
1
+ export type LogMessage = string | (() => string);
2
+ export type LogLevel = "debug" | "info" | "warn" | "error";
3
+ export interface Logger {
4
+ debug(message: LogMessage): void;
5
+ info(message: LogMessage): void;
6
+ warn(message: LogMessage): void;
7
+ error(message: LogMessage): void;
8
+ }
9
+ export interface Formatter {
10
+ format(level: LogLevel, message: string, time?: Date): string;
11
+ }
12
+ export declare class BaseFormatter {
13
+ private readonly tidToken;
14
+ protected tid(): string;
15
+ protected contextString(): string;
16
+ }
17
+ export declare class PrettyFormatter extends BaseFormatter implements Formatter {
18
+ protected colors: Record<string, string>;
19
+ protected useColors: boolean;
20
+ constructor({ useColors }?: {
21
+ useColors?: boolean | undefined;
22
+ });
23
+ protected label(severity: string): string;
24
+ format(level: LogLevel, message: string, time?: Date): string;
25
+ }
26
+ export declare class PlainFormatter extends BaseFormatter implements Formatter {
27
+ format(level: LogLevel, message: string, time?: Date): string;
28
+ }
29
+ export declare class WithoutTimestampFormatter extends PrettyFormatter {
30
+ format(level: LogLevel, message: string): string;
31
+ }
32
+ export declare class JsonFormatter extends BaseFormatter implements Formatter {
33
+ format(level: LogLevel, message: string, time?: Date): string;
34
+ }
35
+ export declare const Formatters: {
36
+ Base: typeof BaseFormatter;
37
+ Pretty: typeof PrettyFormatter;
38
+ Plain: typeof PlainFormatter;
39
+ WithoutTimestamp: typeof WithoutTimestampFormatter;
40
+ JSON: typeof JsonFormatter;
41
+ };
42
+ export declare class SidekiqLogger implements Logger {
43
+ formatter: Formatter;
44
+ private readonly base;
45
+ constructor(base?: Console, formatter?: Formatter);
46
+ debug(message: LogMessage): void;
47
+ info(message: LogMessage): void;
48
+ warn(message: LogMessage): void;
49
+ error(message: LogMessage): void;
50
+ private log;
51
+ }
52
+ export declare const createLogger: (base?: Console) => Logger;
53
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;AACjD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,KAAK,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CAC/D;AA6CD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAE1C,SAAS,CAAC,GAAG,IAAI,MAAM;IAIvB,SAAS,CAAC,aAAa,IAAI,MAAM;CAGlC;AAED,qBAAa,eAAgB,SAAQ,aAAc,YAAW,SAAS;IACrE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEjB,EAAE,SAAyC,EAAE;;KAAK;IAW9D,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAIzC,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,OAAa,GAAG,MAAM;CAOpE;AAED,qBAAa,cAAe,SAAQ,aAAc,YAAW,SAAS;IACpE,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,OAAa,GAAG,MAAM;CAMpE;AAED,qBAAa,yBAA0B,SAAQ,eAAe;IAC5D,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;CAOjD;AAED,qBAAa,aAAc,SAAQ,aAAc,YAAW,SAAS;IACnE,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,OAAa,GAAG,MAAM;CAcpE;AAED,eAAO,MAAM,UAAU;;;;;;CAMtB,CAAC;AAEF,qBAAa,aAAc,YAAW,MAAM;IAC1C,SAAS,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;gBAG7B,IAAI,GAAE,OAAiB,EACvB,SAAS,GAAE,SAA8B;IAM3C,KAAK,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAIhC,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAI/B,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAI/B,KAAK,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAIhC,OAAO,CAAC,GAAG;CAKZ;AASD,eAAO,MAAM,YAAY,GAAI,OAAM,OAAiB,KAAG,MACV,CAAC"}
package/dist/logger.js ADDED
@@ -0,0 +1,143 @@
1
+ import { threadId } from "node:worker_threads";
2
+ import { Context } from "./context.js";
3
+ const resolveMessage = (message) => typeof message === "function" ? message() : message;
4
+ const formatContext = (context) => {
5
+ if (!context) {
6
+ return "";
7
+ }
8
+ const entries = Object.entries(context).filter(([, value]) => value !== undefined);
9
+ if (entries.length === 0) {
10
+ return "";
11
+ }
12
+ return entries
13
+ .map(([key, value]) => {
14
+ if (Array.isArray(value)) {
15
+ return `${key}=${value.join(",")}`;
16
+ }
17
+ return `${key}=${String(value)}`;
18
+ })
19
+ .join(" ");
20
+ };
21
+ const upperLevel = (level) => {
22
+ if (level === "debug") {
23
+ return "DEBUG";
24
+ }
25
+ if (level === "info") {
26
+ return "INFO";
27
+ }
28
+ if (level === "warn") {
29
+ return "WARN";
30
+ }
31
+ return "ERROR";
32
+ };
33
+ const threadToken = () => {
34
+ // biome-ignore lint/suspicious/noBitwiseOperators: XOR for unique thread token
35
+ const token = process.pid ^ (threadId + 1);
36
+ // biome-ignore lint/suspicious/noBitwiseOperators: unsigned right shift for positive value
37
+ return (token >>> 0).toString(36);
38
+ };
39
+ export class BaseFormatter {
40
+ tidToken = threadToken();
41
+ tid() {
42
+ return this.tidToken;
43
+ }
44
+ contextString() {
45
+ return formatContext(Context.peek());
46
+ }
47
+ }
48
+ export class PrettyFormatter extends BaseFormatter {
49
+ colors;
50
+ useColors;
51
+ constructor({ useColors = Boolean(process.stdout.isTTY) } = {}) {
52
+ super();
53
+ this.useColors = useColors;
54
+ this.colors = {
55
+ DEBUG: "\u001b[1;32mDEBUG\u001b[0m",
56
+ INFO: "\u001b[1;34mINFO \u001b[0m",
57
+ WARN: "\u001b[1;33mWARN \u001b[0m",
58
+ ERROR: "\u001b[1;31mERROR\u001b[0m",
59
+ };
60
+ }
61
+ label(severity) {
62
+ return this.useColors ? this.colors[severity] : severity;
63
+ }
64
+ format(level, message, time = new Date()) {
65
+ const severity = upperLevel(level);
66
+ const label = this.label(severity);
67
+ const context = this.contextString();
68
+ const suffix = context ? ` ${context}` : "";
69
+ return `${label} ${time.toISOString()} pid=${process.pid} tid=${this.tid()}${suffix}: ${message}`;
70
+ }
71
+ }
72
+ export class PlainFormatter extends BaseFormatter {
73
+ format(level, message, time = new Date()) {
74
+ const severity = upperLevel(level);
75
+ const context = this.contextString();
76
+ const suffix = context ? ` ${context}` : "";
77
+ return `${severity} ${time.toISOString()} pid=${process.pid} tid=${this.tid()}${suffix}: ${message}`;
78
+ }
79
+ }
80
+ export class WithoutTimestampFormatter extends PrettyFormatter {
81
+ format(level, message) {
82
+ const severity = upperLevel(level);
83
+ const label = this.label(severity);
84
+ const context = this.contextString();
85
+ const suffix = context ? ` ${context}` : "";
86
+ return `${label} pid=${process.pid} tid=${this.tid()}${suffix}: ${message}`;
87
+ }
88
+ }
89
+ export class JsonFormatter extends BaseFormatter {
90
+ format(level, message, time = new Date()) {
91
+ const hash = {
92
+ ts: time.toISOString(),
93
+ pid: process.pid,
94
+ tid: this.tid(),
95
+ lvl: upperLevel(level),
96
+ msg: message,
97
+ };
98
+ const context = Context.peek();
99
+ if (context && Object.keys(context).length > 0) {
100
+ hash.ctx = context;
101
+ }
102
+ return JSON.stringify(hash);
103
+ }
104
+ }
105
+ export const Formatters = {
106
+ Base: BaseFormatter,
107
+ Pretty: PrettyFormatter,
108
+ Plain: PlainFormatter,
109
+ WithoutTimestamp: WithoutTimestampFormatter,
110
+ JSON: JsonFormatter,
111
+ };
112
+ export class SidekiqLogger {
113
+ formatter;
114
+ base;
115
+ constructor(base = console, formatter = defaultFormatter()) {
116
+ this.base = base;
117
+ this.formatter = formatter;
118
+ }
119
+ debug(message) {
120
+ this.log("debug", message);
121
+ }
122
+ info(message) {
123
+ this.log("info", message);
124
+ }
125
+ warn(message) {
126
+ this.log("warn", message);
127
+ }
128
+ error(message) {
129
+ this.log("error", message);
130
+ }
131
+ log(level, message) {
132
+ const resolved = resolveMessage(message);
133
+ const formatted = this.formatter.format(level, resolved);
134
+ this.base[level](formatted);
135
+ }
136
+ }
137
+ const defaultFormatter = () => {
138
+ if (process.env.DYNO) {
139
+ return new WithoutTimestampFormatter();
140
+ }
141
+ return new PrettyFormatter();
142
+ };
143
+ export const createLogger = (base = console) => new SidekiqLogger(base, defaultFormatter());