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.
- package/README.md +686 -0
- package/dist/api.d.ts +172 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +679 -0
- package/dist/backtrace.d.ts +3 -0
- package/dist/backtrace.d.ts.map +1 -0
- package/dist/backtrace.js +16 -0
- package/dist/cli-helpers.d.ts +22 -0
- package/dist/cli-helpers.d.ts.map +1 -0
- package/dist/cli-helpers.js +152 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +143 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +212 -0
- package/dist/config-loader.d.ts +16 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +37 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +155 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +29 -0
- package/dist/cron.d.ts +44 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +173 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/interrupt-handler.d.ts +8 -0
- package/dist/interrupt-handler.d.ts.map +1 -0
- package/dist/interrupt-handler.js +24 -0
- package/dist/iterable-constants.d.ts +3 -0
- package/dist/iterable-constants.d.ts.map +1 -0
- package/dist/iterable-constants.js +2 -0
- package/dist/iterable-errors.d.ts +10 -0
- package/dist/iterable-errors.d.ts.map +1 -0
- package/dist/iterable-errors.js +18 -0
- package/dist/iterable.d.ts +44 -0
- package/dist/iterable.d.ts.map +1 -0
- package/dist/iterable.js +298 -0
- package/dist/job-logger.d.ts +12 -0
- package/dist/job-logger.d.ts.map +1 -0
- package/dist/job-logger.js +64 -0
- package/dist/job-util.d.ts +8 -0
- package/dist/job-util.d.ts.map +1 -0
- package/dist/job-util.js +158 -0
- package/dist/job.d.ts +73 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +200 -0
- package/dist/json.d.ts +3 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +2 -0
- package/dist/leader.d.ts +63 -0
- package/dist/leader.d.ts.map +1 -0
- package/dist/leader.js +193 -0
- package/dist/logger.d.ts +53 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +143 -0
- package/dist/middleware.d.ts +23 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +92 -0
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +205 -0
- package/dist/redis.d.ts +3 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +1 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +8 -0
- package/dist/runner.d.ts +81 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +791 -0
- package/dist/sidekiq.d.ts +43 -0
- package/dist/sidekiq.d.ts.map +1 -0
- package/dist/sidekiq.js +189 -0
- package/dist/testing.d.ts +32 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +112 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Config } from "./config.js";
|
|
2
|
+
export declare const DEFAULT_CONFIG_PATH = "sidekiq.json";
|
|
3
|
+
export interface CliOptions {
|
|
4
|
+
configPath: string;
|
|
5
|
+
configPathProvided: boolean;
|
|
6
|
+
concurrency?: number;
|
|
7
|
+
environment?: string;
|
|
8
|
+
queues?: string[];
|
|
9
|
+
requirePaths?: string[];
|
|
10
|
+
tag?: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
verbose: boolean;
|
|
13
|
+
showHelp: boolean;
|
|
14
|
+
showVersion: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const parseArgs: (argv: string[]) => CliOptions;
|
|
17
|
+
export declare const resolveEnvironment: (cliEnv?: string) => string | undefined;
|
|
18
|
+
export declare const applyCliOptions: (config: Config, options: CliOptions, loadedRequirePaths?: string[]) => {
|
|
19
|
+
config: Config;
|
|
20
|
+
requirePaths: string[];
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=cli-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-helpers.d.ts","sourceRoot":"","sources":["../src/cli-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACtB;AAWD,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,KAAG,UAiH1C,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,SAKvC,CAAC;AASxB,eAAO,MAAM,eAAe,GAC1B,QAAQ,MAAM,EACd,SAAS,UAAU,EACnB,qBAAoB,MAAM,EAAO,KAChC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAuB1C,CAAC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const DEFAULT_CONFIG_PATH = "sidekiq.json";
|
|
2
|
+
const parseNumber = (value, flag) => {
|
|
3
|
+
const parsed = Number(value);
|
|
4
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
5
|
+
throw new Error(`${flag} expects a positive number`);
|
|
6
|
+
}
|
|
7
|
+
return parsed;
|
|
8
|
+
};
|
|
9
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: CLI argument parsing is inherently complex
|
|
10
|
+
export const parseArgs = (argv) => {
|
|
11
|
+
const args = argv.slice(2);
|
|
12
|
+
const options = {
|
|
13
|
+
configPath: DEFAULT_CONFIG_PATH,
|
|
14
|
+
configPathProvided: false,
|
|
15
|
+
verbose: false,
|
|
16
|
+
showHelp: false,
|
|
17
|
+
showVersion: false,
|
|
18
|
+
};
|
|
19
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
20
|
+
const raw = args[i];
|
|
21
|
+
const [arg, inlineValue] = raw.startsWith("--") && raw.includes("=")
|
|
22
|
+
? raw.split("=", 2)
|
|
23
|
+
: [raw, undefined];
|
|
24
|
+
const value = inlineValue ?? args[i + 1];
|
|
25
|
+
switch (arg) {
|
|
26
|
+
case "-h":
|
|
27
|
+
case "--help":
|
|
28
|
+
options.showHelp = true;
|
|
29
|
+
break;
|
|
30
|
+
case "-V":
|
|
31
|
+
case "--version":
|
|
32
|
+
options.showVersion = true;
|
|
33
|
+
break;
|
|
34
|
+
case "-C":
|
|
35
|
+
case "--config":
|
|
36
|
+
if (!value) {
|
|
37
|
+
throw new Error(`${arg} expects a path`);
|
|
38
|
+
}
|
|
39
|
+
options.configPath = value;
|
|
40
|
+
options.configPathProvided = true;
|
|
41
|
+
if (!inlineValue) {
|
|
42
|
+
i += 1;
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
case "-c":
|
|
46
|
+
case "--concurrency":
|
|
47
|
+
if (!value) {
|
|
48
|
+
throw new Error(`${arg} expects a number`);
|
|
49
|
+
}
|
|
50
|
+
options.concurrency = parseNumber(value, arg);
|
|
51
|
+
if (!inlineValue) {
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
case "-e":
|
|
56
|
+
case "--environment":
|
|
57
|
+
if (!value) {
|
|
58
|
+
throw new Error(`${arg} expects a value`);
|
|
59
|
+
}
|
|
60
|
+
options.environment = value;
|
|
61
|
+
if (!inlineValue) {
|
|
62
|
+
i += 1;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case "-g":
|
|
66
|
+
case "--tag":
|
|
67
|
+
if (!value) {
|
|
68
|
+
throw new Error(`${arg} expects a value`);
|
|
69
|
+
}
|
|
70
|
+
options.tag = value;
|
|
71
|
+
if (!inlineValue) {
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case "-q":
|
|
76
|
+
case "--queue":
|
|
77
|
+
if (!value) {
|
|
78
|
+
throw new Error(`${arg} expects a queue name`);
|
|
79
|
+
}
|
|
80
|
+
options.queues ??= [];
|
|
81
|
+
options.queues.push(value);
|
|
82
|
+
if (!inlineValue) {
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case "-r":
|
|
87
|
+
case "--require":
|
|
88
|
+
if (!value) {
|
|
89
|
+
throw new Error(`${arg} expects a path`);
|
|
90
|
+
}
|
|
91
|
+
options.requirePaths ??= [];
|
|
92
|
+
options.requirePaths.push(value);
|
|
93
|
+
if (!inlineValue) {
|
|
94
|
+
i += 1;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case "-t":
|
|
98
|
+
case "--timeout":
|
|
99
|
+
if (!value) {
|
|
100
|
+
throw new Error(`${arg} expects a number`);
|
|
101
|
+
}
|
|
102
|
+
options.timeout = parseNumber(value, arg);
|
|
103
|
+
if (!inlineValue) {
|
|
104
|
+
i += 1;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
case "-v":
|
|
108
|
+
case "--verbose":
|
|
109
|
+
options.verbose = true;
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
if (arg.startsWith("-")) {
|
|
113
|
+
throw new Error(`Unknown option ${arg}`);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return options;
|
|
119
|
+
};
|
|
120
|
+
export const resolveEnvironment = (cliEnv) => cliEnv ??
|
|
121
|
+
process.env.APP_ENV ??
|
|
122
|
+
process.env.NODE_ENV ??
|
|
123
|
+
process.env.RACK_ENV ??
|
|
124
|
+
process.env.RAILS_ENV;
|
|
125
|
+
const suppressDebug = (logger) => ({
|
|
126
|
+
debug: () => undefined,
|
|
127
|
+
info: logger.info.bind(logger),
|
|
128
|
+
warn: logger.warn.bind(logger),
|
|
129
|
+
error: logger.error.bind(logger),
|
|
130
|
+
});
|
|
131
|
+
export const applyCliOptions = (config, options, loadedRequirePaths = []) => {
|
|
132
|
+
let requirePaths = loadedRequirePaths;
|
|
133
|
+
if (options.concurrency !== undefined) {
|
|
134
|
+
config.concurrency = options.concurrency;
|
|
135
|
+
}
|
|
136
|
+
if (options.queues && options.queues.length > 0) {
|
|
137
|
+
config.queues = options.queues;
|
|
138
|
+
}
|
|
139
|
+
if (options.tag) {
|
|
140
|
+
config.tag = options.tag;
|
|
141
|
+
}
|
|
142
|
+
if (options.timeout !== undefined) {
|
|
143
|
+
config.timeout = options.timeout;
|
|
144
|
+
}
|
|
145
|
+
if (!options.verbose) {
|
|
146
|
+
config.logger = suppressDebug(config.logger);
|
|
147
|
+
}
|
|
148
|
+
if (options.requirePaths && options.requirePaths.length > 0) {
|
|
149
|
+
requirePaths = options.requirePaths;
|
|
150
|
+
}
|
|
151
|
+
return { config, requirePaths };
|
|
152
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { applyCliOptions, parseArgs, resolveEnvironment, } from "./cli-helpers.js";
|
|
7
|
+
import { Config } from "./config.js";
|
|
8
|
+
import { loadConfigFile } from "./config-loader.js";
|
|
9
|
+
import { Sidekiq } from "./sidekiq.js";
|
|
10
|
+
const printHelp = () => {
|
|
11
|
+
console.log(`Usage: sidekiq-ts [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-C, --config PATH Path to JSON config file (default: sidekiq.json if present)
|
|
15
|
+
-c, --concurrency NUM Processor threads to use
|
|
16
|
+
-e, --environment ENV Application environment
|
|
17
|
+
-g, --tag TAG Process tag
|
|
18
|
+
-q, --queue QUEUE[,WT] Queue with optional weight (repeatable)
|
|
19
|
+
-r, --require PATH File or directory to import before startup (repeatable)
|
|
20
|
+
-t, --timeout NUM Shutdown timeout seconds
|
|
21
|
+
-v, --verbose Enable debug logging
|
|
22
|
+
-V, --version Print version and exit
|
|
23
|
+
-h, --help Show this help message
|
|
24
|
+
`);
|
|
25
|
+
};
|
|
26
|
+
const fileExists = async (path) => {
|
|
27
|
+
try {
|
|
28
|
+
await access(path);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const printVersion = () => {
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const pkg = require("../package.json");
|
|
38
|
+
const name = pkg.name ?? "sidekiq-ts";
|
|
39
|
+
const version = pkg.version ?? "0.0.0";
|
|
40
|
+
console.log(`${name} ${version}`);
|
|
41
|
+
};
|
|
42
|
+
const registerSignal = (signal, handler) => {
|
|
43
|
+
try {
|
|
44
|
+
process.on(signal, () => {
|
|
45
|
+
Promise.resolve(handler()).catch(() => undefined);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Signal not supported on this platform.
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const main = async () => {
|
|
53
|
+
const options = parseArgs(process.argv);
|
|
54
|
+
if (options.showHelp) {
|
|
55
|
+
printHelp();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (options.showVersion) {
|
|
59
|
+
printVersion();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const environment = resolveEnvironment(options.environment);
|
|
63
|
+
if (environment && !process.env.NODE_ENV) {
|
|
64
|
+
process.env.NODE_ENV = environment;
|
|
65
|
+
}
|
|
66
|
+
let config = new Config();
|
|
67
|
+
let requirePaths = [];
|
|
68
|
+
const configExists = await fileExists(options.configPath);
|
|
69
|
+
if (options.configPathProvided || configExists) {
|
|
70
|
+
if (!configExists) {
|
|
71
|
+
throw new Error(`No such file ${options.configPath}`);
|
|
72
|
+
}
|
|
73
|
+
const loaded = await loadConfigFile(options.configPath, { environment });
|
|
74
|
+
config = loaded.config;
|
|
75
|
+
requirePaths = loaded.requirePaths;
|
|
76
|
+
}
|
|
77
|
+
({ config, requirePaths } = applyCliOptions(config, options, requirePaths));
|
|
78
|
+
for (const entry of requirePaths) {
|
|
79
|
+
const fullPath = resolve(entry);
|
|
80
|
+
await import(pathToFileURL(fullPath).href);
|
|
81
|
+
}
|
|
82
|
+
const runner = await Sidekiq.run({ config });
|
|
83
|
+
let shuttingDown = false;
|
|
84
|
+
let quieting = false;
|
|
85
|
+
const shutdown = async (signal) => {
|
|
86
|
+
if (shuttingDown) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
shuttingDown = true;
|
|
90
|
+
config.logger.info(() => `Received ${signal}, shutting down`);
|
|
91
|
+
await runner.stop();
|
|
92
|
+
process.exit(0);
|
|
93
|
+
};
|
|
94
|
+
const quiet = async (signal) => {
|
|
95
|
+
if (quieting) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
quieting = true;
|
|
99
|
+
config.logger.info(() => `Received ${signal}, no longer accepting new work`);
|
|
100
|
+
await runner.quiet();
|
|
101
|
+
};
|
|
102
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: signal handling needs detailed logging
|
|
103
|
+
const dumpState = (signal) => {
|
|
104
|
+
config.logger.warn(() => `Received ${signal}, dumping state`);
|
|
105
|
+
const snapshot = runner.snapshotWork();
|
|
106
|
+
if (snapshot.length === 0) {
|
|
107
|
+
config.logger.warn(() => "No active jobs");
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
config.logger.warn(() => `Active jobs: ${snapshot.length}`);
|
|
111
|
+
for (const entry of snapshot) {
|
|
112
|
+
const payload = entry.payload;
|
|
113
|
+
let className;
|
|
114
|
+
if (!payload) {
|
|
115
|
+
className = "unknown";
|
|
116
|
+
}
|
|
117
|
+
else if (typeof payload.class === "string") {
|
|
118
|
+
className = payload.class;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
className = payload.class?.name ?? String(payload.class);
|
|
122
|
+
}
|
|
123
|
+
const jid = payload?.jid ?? "unknown";
|
|
124
|
+
const elapsed = entry.elapsed.toFixed(3);
|
|
125
|
+
config.logger.warn(() => `Worker ${entry.workerId} class=${className} jid=${jid} queue=${entry.queue} elapsed=${elapsed}s`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const stack = new Error("Signal stack").stack;
|
|
129
|
+
if (stack) {
|
|
130
|
+
const lines = stack.split("\n").slice(1).join("\n");
|
|
131
|
+
config.logger.warn(() => `Current stack:\n${lines}`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
registerSignal("SIGINT", () => shutdown("SIGINT"));
|
|
135
|
+
registerSignal("SIGTERM", () => shutdown("SIGTERM"));
|
|
136
|
+
registerSignal("SIGTSTP", () => quiet("SIGTSTP"));
|
|
137
|
+
registerSignal("SIGTTIN", () => dumpState("SIGTTIN"));
|
|
138
|
+
registerSignal("SIGINFO", () => dumpState("SIGINFO"));
|
|
139
|
+
};
|
|
140
|
+
main().catch((error) => {
|
|
141
|
+
console.error(error);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Config } from "./config.js";
|
|
2
|
+
import type { RedisClient } from "./redis.js";
|
|
3
|
+
import type { BulkPayload, JobClassLike, JobPayload } from "./types.js";
|
|
4
|
+
export declare class Client {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly redisClient?;
|
|
7
|
+
constructor({ config, redis, }?: {
|
|
8
|
+
config?: Config;
|
|
9
|
+
redis?: RedisClient;
|
|
10
|
+
});
|
|
11
|
+
private getRedis;
|
|
12
|
+
middleware(fn?: (chain: Config["clientMiddleware"]) => void): Config["clientMiddleware"];
|
|
13
|
+
push(item: JobPayload): Promise<string | null>;
|
|
14
|
+
pushBulk(items: BulkPayload): Promise<(string | null)[]>;
|
|
15
|
+
cancel(jid: string): Promise<boolean>;
|
|
16
|
+
static push(item: JobPayload): Promise<string | null>;
|
|
17
|
+
static pushBulk(items: BulkPayload): Promise<(string | null)[]>;
|
|
18
|
+
static enqueue<TArgs extends unknown[]>(klass: JobClassLike, ...args: TArgs): Promise<string | null>;
|
|
19
|
+
static enqueueTo<TArgs extends unknown[]>(queue: string, klass: JobClassLike, ...args: TArgs): Promise<string | null>;
|
|
20
|
+
static enqueueToIn<TArgs extends unknown[]>(queue: string, interval: number, klass: JobClassLike, ...args: TArgs): Promise<string | null>;
|
|
21
|
+
static enqueueIn<TArgs extends unknown[]>(interval: number, klass: JobClassLike, ...args: TArgs): Promise<string | null>;
|
|
22
|
+
static via<T>(redis: RedisClient, fn: () => Promise<T>): Promise<T>;
|
|
23
|
+
private rawPush;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAS1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAIxE,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAc;gBAE/B,EACV,MAAM,EACN,KAAK,GACN,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,WAAW,CAAA;KAAO;YAKlC,QAAQ;IAQtB,UAAU,CACR,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,kBAAkB,CAAC,KAAK,IAAI,GAC/C,MAAM,CAAC,kBAAkB,CAAC;IAOvB,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAqB9C,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IA0GxD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3C,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIrD,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAI/D,MAAM,CAAC,OAAO,CAAC,KAAK,SAAS,OAAO,EAAE,EACpC,KAAK,EAAE,YAAY,EACnB,GAAG,IAAI,EAAE,KAAK,GACb,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIzB,MAAM,CAAC,SAAS,CAAC,KAAK,SAAS,OAAO,EAAE,EACtC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,YAAY,EACnB,GAAG,IAAI,EAAE,KAAK,GACb,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIzB,MAAM,CAAC,WAAW,CAAC,KAAK,SAAS,OAAO,EAAE,EACxC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,EACnB,GAAG,IAAI,EAAE,KAAK,GACb,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUzB,MAAM,CAAC,SAAS,CAAC,KAAK,SAAS,OAAO,EAAE,EACtC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,EACnB,GAAG,IAAI,EAAE,KAAK,GACb,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQzB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;YAKrD,OAAO;CAoDtB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { ITERATION_STATE_TTL_SECONDS } from "./iterable-constants.js";
|
|
3
|
+
import { generateJid, normalizeItem, nowInMillis, verifyJson, } from "./job-util.js";
|
|
4
|
+
import { dumpJson } from "./json.js";
|
|
5
|
+
import { Sidekiq } from "./sidekiq.js";
|
|
6
|
+
import { Testing } from "./testing.js";
|
|
7
|
+
const redisContext = new AsyncLocalStorage();
|
|
8
|
+
export class Client {
|
|
9
|
+
config;
|
|
10
|
+
redisClient;
|
|
11
|
+
constructor({ config, redis, } = {}) {
|
|
12
|
+
this.config = config ?? Sidekiq.defaultConfiguration;
|
|
13
|
+
this.redisClient = redis;
|
|
14
|
+
}
|
|
15
|
+
async getRedis() {
|
|
16
|
+
return (this.redisClient ??
|
|
17
|
+
redisContext.getStore() ??
|
|
18
|
+
(await this.config.getRedisClient()));
|
|
19
|
+
}
|
|
20
|
+
middleware(fn) {
|
|
21
|
+
if (fn) {
|
|
22
|
+
fn(this.config.clientMiddleware);
|
|
23
|
+
}
|
|
24
|
+
return this.config.clientMiddleware;
|
|
25
|
+
}
|
|
26
|
+
async push(item) {
|
|
27
|
+
const normalized = normalizeItem(item, Sidekiq.defaultJobOptions());
|
|
28
|
+
const queue = normalized.queue ?? "default";
|
|
29
|
+
const redis = await this.getRedis();
|
|
30
|
+
const result = await this.config.clientMiddleware.invoke(item.class, normalized, queue, redis, async () => normalized);
|
|
31
|
+
if (!result) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const payload = result;
|
|
35
|
+
verifyJson(payload.args, this.config.strictArgs);
|
|
36
|
+
await this.rawPush([payload]);
|
|
37
|
+
return payload.jid ?? null;
|
|
38
|
+
}
|
|
39
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: bulk push validation is inherently complex
|
|
40
|
+
async pushBulk(items) {
|
|
41
|
+
const batchSize = items.batch_size ?? 1000;
|
|
42
|
+
const args = items.args;
|
|
43
|
+
const at = items.at;
|
|
44
|
+
if (!Array.isArray(args)) {
|
|
45
|
+
throw new Error("Bulk arguments must be an Array of Arrays");
|
|
46
|
+
}
|
|
47
|
+
if (at !== undefined) {
|
|
48
|
+
const atArray = Array.isArray(at) ? at : [at];
|
|
49
|
+
if (atArray.length === 0 ||
|
|
50
|
+
!atArray.every((entry) => typeof entry === "number")) {
|
|
51
|
+
throw new Error("Job 'at' must be a number or array of numbers");
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(at) && at.length !== args.length) {
|
|
54
|
+
throw new Error("Job 'at' array must match args array size");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const jid = items.jid;
|
|
58
|
+
if (jid && args.length > 1) {
|
|
59
|
+
throw new Error("Explicit 'jid' is not supported for bulk jobs");
|
|
60
|
+
}
|
|
61
|
+
const spreadInterval = items.spread_interval;
|
|
62
|
+
if (spreadInterval !== undefined &&
|
|
63
|
+
(typeof spreadInterval !== "number" || spreadInterval <= 0)) {
|
|
64
|
+
throw new Error("Jobs 'spread_interval' must be a positive number");
|
|
65
|
+
}
|
|
66
|
+
if (at !== undefined && spreadInterval !== undefined) {
|
|
67
|
+
throw new Error("Only one of 'at' or 'spread_interval' can be provided");
|
|
68
|
+
}
|
|
69
|
+
let resolvedAt = at;
|
|
70
|
+
if (resolvedAt === undefined && spreadInterval !== undefined) {
|
|
71
|
+
const interval = Math.max(spreadInterval, 5);
|
|
72
|
+
const now = Date.now() / 1000;
|
|
73
|
+
resolvedAt = args.map(() => now + Math.random() * interval);
|
|
74
|
+
}
|
|
75
|
+
const base = {
|
|
76
|
+
...items,
|
|
77
|
+
args,
|
|
78
|
+
};
|
|
79
|
+
base.batch_size = undefined;
|
|
80
|
+
base.spread_interval = undefined;
|
|
81
|
+
base.at = undefined;
|
|
82
|
+
base.jid = undefined;
|
|
83
|
+
const normalized = normalizeItem(base, Sidekiq.defaultJobOptions());
|
|
84
|
+
const results = [];
|
|
85
|
+
const redis = await this.getRedis();
|
|
86
|
+
for (let i = 0; i < args.length; i += batchSize) {
|
|
87
|
+
const slice = args.slice(i, i + batchSize);
|
|
88
|
+
if (slice.length === 0) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (!slice.every((entry) => Array.isArray(entry))) {
|
|
92
|
+
throw new Error("Bulk arguments must be an Array of Arrays: [[1], [2]]");
|
|
93
|
+
}
|
|
94
|
+
const payloads = await Promise.all(slice.map(async (jobArgs, index) => {
|
|
95
|
+
const payload = {
|
|
96
|
+
...normalized,
|
|
97
|
+
args: jobArgs,
|
|
98
|
+
jid: generateJid(),
|
|
99
|
+
};
|
|
100
|
+
if (resolvedAt !== undefined) {
|
|
101
|
+
payload.at = Array.isArray(resolvedAt)
|
|
102
|
+
? resolvedAt[i + index]
|
|
103
|
+
: resolvedAt;
|
|
104
|
+
}
|
|
105
|
+
const result = await this.config.clientMiddleware.invoke(items.class, payload, payload.queue ?? "default", redis, async () => payload);
|
|
106
|
+
if (!result) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const finalPayload = result;
|
|
110
|
+
verifyJson(finalPayload.args, this.config.strictArgs);
|
|
111
|
+
return finalPayload;
|
|
112
|
+
}));
|
|
113
|
+
const toPush = payloads.filter((payload) => Boolean(payload));
|
|
114
|
+
await this.rawPush(toPush);
|
|
115
|
+
results.push(...payloads.map((payload) => payload?.jid ?? null));
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
async cancel(jid) {
|
|
120
|
+
const redis = await this.getRedis();
|
|
121
|
+
const key = `it-${jid}`;
|
|
122
|
+
const now = String(Math.floor(Date.now() / 1000));
|
|
123
|
+
const pipeline = redis.multi();
|
|
124
|
+
pipeline.hSetNX(key, "cancelled", now);
|
|
125
|
+
pipeline.hGet(key, "cancelled");
|
|
126
|
+
pipeline.expire(key, ITERATION_STATE_TTL_SECONDS, "NX");
|
|
127
|
+
const result = await pipeline.exec();
|
|
128
|
+
const cancelled = result?.[1];
|
|
129
|
+
return Boolean(Number(cancelled));
|
|
130
|
+
}
|
|
131
|
+
// biome-ignore lint/suspicious/useAdjacentOverloadSignatures: static methods are grouped separately from instance methods
|
|
132
|
+
static push(item) {
|
|
133
|
+
return new Client().push(item);
|
|
134
|
+
}
|
|
135
|
+
static pushBulk(items) {
|
|
136
|
+
return new Client().pushBulk(items);
|
|
137
|
+
}
|
|
138
|
+
static enqueue(klass, ...args) {
|
|
139
|
+
return Client.push({ class: klass, args });
|
|
140
|
+
}
|
|
141
|
+
static enqueueTo(queue, klass, ...args) {
|
|
142
|
+
return Client.push({ class: klass, queue, args });
|
|
143
|
+
}
|
|
144
|
+
static enqueueToIn(queue, interval, klass, ...args) {
|
|
145
|
+
const now = Date.now() / 1000;
|
|
146
|
+
const ts = interval < 1_000_000_000 ? now + interval : interval;
|
|
147
|
+
const payload = { class: klass, queue, args };
|
|
148
|
+
if (ts > now) {
|
|
149
|
+
payload.at = ts;
|
|
150
|
+
}
|
|
151
|
+
return Client.push(payload);
|
|
152
|
+
}
|
|
153
|
+
static enqueueIn(interval, klass, ...args) {
|
|
154
|
+
const queue = typeof klass !== "string" && klass.getSidekiqOptions
|
|
155
|
+
? (klass.getSidekiqOptions().queue ?? "default")
|
|
156
|
+
: "default";
|
|
157
|
+
return Client.enqueueToIn(queue, interval, klass, ...args);
|
|
158
|
+
}
|
|
159
|
+
static via(redis, fn) {
|
|
160
|
+
return redisContext.run(redis, fn);
|
|
161
|
+
}
|
|
162
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: push logic requires handling multiple cases
|
|
163
|
+
async rawPush(payloads) {
|
|
164
|
+
if (payloads.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const testMode = Testing.mode();
|
|
168
|
+
if (testMode === "fake") {
|
|
169
|
+
for (const payload of payloads) {
|
|
170
|
+
Testing.enqueue(payload);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (testMode === "inline") {
|
|
175
|
+
for (const payload of payloads) {
|
|
176
|
+
await Testing.performInline(payload, this.config);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const redis = await this.getRedis();
|
|
181
|
+
const pipeline = redis.multi();
|
|
182
|
+
if (payloads[0].at !== undefined) {
|
|
183
|
+
for (const payload of payloads) {
|
|
184
|
+
const at = payload.at;
|
|
185
|
+
const { at: _at, enqueued_at: _enqueuedAt, ...copy } = payload;
|
|
186
|
+
pipeline.zAdd("schedule", [{ score: at, value: dumpJson(copy) }]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const now = nowInMillis();
|
|
191
|
+
const grouped = new Map();
|
|
192
|
+
for (const payload of payloads) {
|
|
193
|
+
const queue = payload.queue;
|
|
194
|
+
const bucket = grouped.get(queue) ?? [];
|
|
195
|
+
bucket.push(payload);
|
|
196
|
+
grouped.set(queue, bucket);
|
|
197
|
+
}
|
|
198
|
+
const queueNames = Array.from(grouped.keys());
|
|
199
|
+
if (queueNames.length > 0) {
|
|
200
|
+
pipeline.sAdd("queues", queueNames);
|
|
201
|
+
}
|
|
202
|
+
for (const [queue, entries] of grouped.entries()) {
|
|
203
|
+
const toPush = entries.map((entry) => {
|
|
204
|
+
entry.enqueued_at = now;
|
|
205
|
+
return dumpJson(entry);
|
|
206
|
+
});
|
|
207
|
+
pipeline.lPush(`queue:${queue}`, toPush);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
await pipeline.exec();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Config } from "./config.js";
|
|
2
|
+
import type { ConfigOptions } from "./types.js";
|
|
3
|
+
export interface JsonConfig extends ConfigOptions {
|
|
4
|
+
require?: string | string[];
|
|
5
|
+
environments?: Record<string, JsonConfig>;
|
|
6
|
+
}
|
|
7
|
+
export interface LoadedConfig {
|
|
8
|
+
config: Config;
|
|
9
|
+
requirePaths: string[];
|
|
10
|
+
sourcePath: string;
|
|
11
|
+
}
|
|
12
|
+
export interface LoadConfigOptions {
|
|
13
|
+
environment?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const loadConfigFile: (path: string, options?: LoadConfigOptions) => Promise<LoadedConfig>;
|
|
16
|
+
//# sourceMappingURL=config-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../src/config-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA4BD,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EACZ,UAAS,iBAAsB,KAC9B,OAAO,CAAC,YAAY,CActB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Config } from "./config.js";
|
|
4
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
const normalizeRequire = (value) => {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.filter((entry) => typeof entry === "string");
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
return [value];
|
|
11
|
+
}
|
|
12
|
+
return [];
|
|
13
|
+
};
|
|
14
|
+
const resolveEnvironmentOverrides = (parsed, environment) => {
|
|
15
|
+
if (!environment) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const direct = isRecord(parsed[environment]) ? parsed[environment] : {};
|
|
19
|
+
const envMap = isRecord(parsed.environments) ? parsed.environments : {};
|
|
20
|
+
const mapped = isRecord(envMap[environment]) ? envMap[environment] : {};
|
|
21
|
+
return { ...direct, ...mapped };
|
|
22
|
+
};
|
|
23
|
+
export const loadConfigFile = async (path, options = {}) => {
|
|
24
|
+
const fullPath = resolve(path);
|
|
25
|
+
const raw = await readFile(fullPath, "utf8");
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
const envOverrides = resolveEnvironmentOverrides(parsed, options.environment);
|
|
28
|
+
const merged = { ...parsed, ...envOverrides };
|
|
29
|
+
const requirePaths = normalizeRequire(merged.require);
|
|
30
|
+
merged.require = undefined;
|
|
31
|
+
merged.environments = undefined;
|
|
32
|
+
if (options.environment) {
|
|
33
|
+
merged[options.environment] = undefined;
|
|
34
|
+
}
|
|
35
|
+
const config = new Config(merged);
|
|
36
|
+
return { config, requirePaths, sourcePath: fullPath };
|
|
37
|
+
};
|