@oneunit/redis 0.0.0-stage → 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/ARCHITECTURE.md +422 -0
- package/CHANGELOG.md +186 -0
- package/CONTRIBUTING.md +353 -0
- package/LICENSE +21 -0
- package/README.md +760 -2
- package/dist/client/check.d.ts +19 -0
- package/dist/client/check.d.ts.map +1 -0
- package/dist/client/check.js +44 -0
- package/dist/client/check.js.map +1 -0
- package/dist/client/client.d.ts +10 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +25 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/events.d.ts +5 -0
- package/dist/client/events.d.ts.map +1 -0
- package/dist/client/events.js +66 -0
- package/dist/client/events.js.map +1 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/shutdown.d.ts +4 -0
- package/dist/client/shutdown.d.ts.map +1 -0
- package/dist/client/shutdown.js +78 -0
- package/dist/client/shutdown.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +120 -0
- package/dist/logger.js.map +1 -0
- package/dist/pipeline/builder.d.ts +111 -0
- package/dist/pipeline/builder.d.ts.map +1 -0
- package/dist/pipeline/builder.js +197 -0
- package/dist/pipeline/builder.js.map +1 -0
- package/dist/pipeline/index.d.ts +3 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/queue/events.d.ts +13 -0
- package/dist/queue/events.d.ts.map +1 -0
- package/dist/queue/events.js +109 -0
- package/dist/queue/events.js.map +1 -0
- package/dist/queue/index.d.ts +7 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +4 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.d.ts +13 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +37 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/worker.d.ts +15 -0
- package/dist/queue/worker.d.ts.map +1 -0
- package/dist/queue/worker.js +20 -0
- package/dist/queue/worker.js.map +1 -0
- package/examples/README.md +86 -0
- package/examples/_setup.js +143 -0
- package/examples/cache.js +111 -0
- package/examples/pipeline.js +161 -0
- package/examples/pubsub.js +101 -0
- package/examples/queue-worker.js +189 -0
- package/examples/session.js +145 -0
- package/examples/standalone.js +58 -0
- package/package.json +100 -4
- package/src/client/check.ts +69 -0
- package/src/client/client.ts +45 -0
- package/src/client/events.ts +101 -0
- package/src/client/index.ts +5 -0
- package/src/client/shutdown.ts +97 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +159 -0
- package/src/pipeline/builder.ts +307 -0
- package/src/pipeline/index.ts +7 -0
- package/src/queue/events.ts +158 -0
- package/src/queue/index.ts +6 -0
- package/src/queue/queue.ts +60 -0
- package/src/queue/worker.ts +44 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Redis as RedisClient } from "ioredis";
|
|
2
|
+
export interface HealthResult {
|
|
3
|
+
status: "up" | "down";
|
|
4
|
+
latency: {
|
|
5
|
+
value: number;
|
|
6
|
+
unit: "ms";
|
|
7
|
+
};
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HealthOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Milliseconds to wait for PING before reporting `down`. ioredis queues
|
|
13
|
+
* commands while reconnecting, so without a bound a PING against an
|
|
14
|
+
* unreachable server never settles and health checks hang forever.
|
|
15
|
+
*/
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function health(client: RedisClient, options?: HealthOptions): Promise<HealthResult>;
|
|
19
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/client/check.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,KAAK,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;IACtB,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,IAAI,CAAC;KACZ,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,wBAAsB,MAAM,CAC1B,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA0CvB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import {} from "ioredis";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 1000;
|
|
4
|
+
export async function health(client, options = {}) {
|
|
5
|
+
const requested = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
6
|
+
// setTimeout coerces negatives to 1 and NaN to 1, and Node prints a
|
|
7
|
+
// TimeoutNegativeWarning/TimeoutNaNWarning for each. Treat any invalid
|
|
8
|
+
// budget as "no bound configured" rather than leaking warnings per call.
|
|
9
|
+
const timeout = Number.isFinite(requested) && requested > 0
|
|
10
|
+
? requested
|
|
11
|
+
: DEFAULT_TIMEOUT_MS;
|
|
12
|
+
const start = performance.now();
|
|
13
|
+
const elapsed = () => Math.round(performance.now() - start);
|
|
14
|
+
let timer;
|
|
15
|
+
const expiry = new Promise((_resolve, reject) => {
|
|
16
|
+
timer = setTimeout(() => {
|
|
17
|
+
reject(new Error(`Health check timed out after ${timeout}ms`));
|
|
18
|
+
}, timeout);
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
await Promise.race([client.ping(), expiry]);
|
|
22
|
+
return {
|
|
23
|
+
status: "up",
|
|
24
|
+
latency: {
|
|
25
|
+
value: elapsed(),
|
|
26
|
+
unit: "ms",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
status: "down",
|
|
33
|
+
latency: {
|
|
34
|
+
value: elapsed(),
|
|
35
|
+
unit: "ms",
|
|
36
|
+
},
|
|
37
|
+
error: error instanceof Error ? error.message : String(error),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/client/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAA6B,MAAM,SAAS,CAAC;AAoBpD,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,MAAmB,EACnB,UAAyB,EAAE;IAE3B,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,IAAI,kBAAkB,CAAC;IACxD,oEAAoE;IACpE,uEAAuE;IACvE,yEAAyE;IACzE,MAAM,OAAO,GACX,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC;QACzC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,kBAAkB,CAAC;IACzB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAEhC,MAAM,OAAO,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;IAEpE,IAAI,KAAiC,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,OAAO,CAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE;QACrD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,OAAO,IAAI,CAAC,CAAC,CAAC;QACjE,CAAC,EAAE,OAAO,CAAC,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAE5C,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;gBACP,KAAK,EAAE,OAAO,EAAE;gBAChB,IAAI,EAAE,IAAI;aACX;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,KAAK,EAAE,OAAO,EAAE;gBAChB,IAAI,EAAE,IAAI;aACX;YACD,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAC9D,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
import { type Logger } from "../logger.js";
|
|
3
|
+
import type { RedisOptions } from "ioredis";
|
|
4
|
+
export interface RedisClientOptions extends RedisOptions {
|
|
5
|
+
url?: string;
|
|
6
|
+
lazyConnect?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function createClient(options?: RedisClientOptions | string, logger?: Logger): Redis;
|
|
9
|
+
export type { Logger } from "../logger.js";
|
|
10
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,kBAAkB,GAAG,MAAW,EACzC,MAAM,CAAC,EAAE,MAAM,GACd,KAAK,CA6BP;AAED,YAAY,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
import { attachEvents } from "./events.js";
|
|
3
|
+
import { normalizeLogger } from "../logger.js";
|
|
4
|
+
export function createClient(options = {}, logger) {
|
|
5
|
+
// ioredis's own constructor accepts `new Redis("redis://host:port")`, so
|
|
6
|
+
// that is the form people reach for first. Destructuring the string as an
|
|
7
|
+
// options object produces no `url`, which leaves the client pointing at
|
|
8
|
+
// localhost:6379: a silent connection to the wrong server, with no error
|
|
9
|
+
// anywhere to notice it by.
|
|
10
|
+
const resolved = typeof options === "string" ? { url: options } : options;
|
|
11
|
+
const { url = process.env.REDIS_URL, lazyConnect = true,
|
|
12
|
+
// BullMQ throws unless this is null on the connections it drives, and
|
|
13
|
+
// `createQueue`/`createWorker` take this same client instance.
|
|
14
|
+
maxRetriesPerRequest = null, ...restOptions } = resolved;
|
|
15
|
+
// ioredis only reads `url` from the first positional argument; an options
|
|
16
|
+
// object carrying `url` is silently ignored and connects to localhost.
|
|
17
|
+
const client = url
|
|
18
|
+
? new Redis(url, { ...restOptions, lazyConnect, maxRetriesPerRequest })
|
|
19
|
+
: new Redis({ ...restOptions, lazyConnect, maxRetriesPerRequest });
|
|
20
|
+
// A caller-supplied logger may implement only some of the levels. Completing
|
|
21
|
+
// it here keeps `attachEvents` from throwing mid-connection-event.
|
|
22
|
+
attachEvents(client, normalizeLogger(logger));
|
|
23
|
+
return client;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAe,MAAM,cAAc,CAAC;AAQ5D,MAAM,UAAU,YAAY,CAC1B,UAAuC,EAAE,EACzC,MAAe;IAEf,yEAAyE;IACzE,0EAA0E;IAC1E,wEAAwE;IACxE,yEAAyE;IACzE,4BAA4B;IAC5B,MAAM,QAAQ,GACZ,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;IAE3D,MAAM,EACJ,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,EAC3B,WAAW,GAAG,IAAI;IAClB,sEAAsE;IACtE,+DAA+D;IAC/D,oBAAoB,GAAG,IAAI,EAC3B,GAAG,WAAW,EACf,GAAG,QAAQ,CAAC;IAEb,0EAA0E;IAC1E,uEAAuE;IACvE,MAAM,MAAM,GAAG,GAAG;QAChB,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;QACvE,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,GAAG,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC,CAAC;IAErE,6EAA6E;IAC7E,mEAAmE;IACnE,YAAY,CAAC,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;IAE9C,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Logger } from "../logger.js";
|
|
2
|
+
import { type Redis as RedisClient } from "ioredis";
|
|
3
|
+
export declare function redactError(error: unknown): unknown;
|
|
4
|
+
export declare function attachEvents(client: RedisClient, loggerInput?: Logger): void;
|
|
5
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/client/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,KAAK,KAAK,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AAsBpD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2BnD;AAiBD,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAiC5E"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { normalizeLogger } from "../logger.js";
|
|
2
|
+
import {} from "ioredis";
|
|
3
|
+
/** Command names whose arguments must never be logged. */
|
|
4
|
+
const SECRET_COMMANDS = new Set(["auth", "hello"]);
|
|
5
|
+
export function redactError(error) {
|
|
6
|
+
if (!(error instanceof Error)) {
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
const command = error.command;
|
|
10
|
+
if (typeof command === "object" &&
|
|
11
|
+
command !== null &&
|
|
12
|
+
SECRET_COMMANDS.has(String(command.name).toLowerCase())) {
|
|
13
|
+
// Copy rather than mutate: ioredis may still be using this error, and the
|
|
14
|
+
// caller has no reason to lose the original for their own handling.
|
|
15
|
+
//
|
|
16
|
+
// `message` is deliberately kept. Redis error text is what tells an
|
|
17
|
+
// operator *why* authentication failed, and it never echoes the password
|
|
18
|
+
// back. Only the attached command's args carry it.
|
|
19
|
+
const safe = new Error(error.message);
|
|
20
|
+
safe.name = error.name;
|
|
21
|
+
safe.stack = error.stack;
|
|
22
|
+
safe.command = { ...command, args: "[redacted]" };
|
|
23
|
+
return safe;
|
|
24
|
+
}
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Marks a client this function has already wired up.
|
|
29
|
+
*
|
|
30
|
+
* `attachEvents` is exported and `createClient` calls it. A caller reaching for
|
|
31
|
+
* the exported helper on a client from `createClient` would otherwise get a
|
|
32
|
+
* second full set of listeners, logging every connection event twice. BullMQ
|
|
33
|
+
* adds listeners to the same client too, so crossing the default limit of 10
|
|
34
|
+
* and tripping `MaxListenersExceededWarning` should not be something a
|
|
35
|
+
* consumer causes by accident.
|
|
36
|
+
*
|
|
37
|
+
* A global symbol is used so two copies of this package in one dependency tree
|
|
38
|
+
* still recognise a client the other already wired.
|
|
39
|
+
*/
|
|
40
|
+
const WIRED = Symbol.for("oneunit.redis.eventsAttached");
|
|
41
|
+
export function attachEvents(client, loggerInput) {
|
|
42
|
+
if (client[WIRED]) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
Object.defineProperty(client, WIRED, {
|
|
46
|
+
value: true,
|
|
47
|
+
enumerable: false,
|
|
48
|
+
});
|
|
49
|
+
const logger = normalizeLogger(loggerInput);
|
|
50
|
+
client.on("connect", () => {
|
|
51
|
+
logger?.info("redis connect");
|
|
52
|
+
});
|
|
53
|
+
client.on("ready", () => {
|
|
54
|
+
logger?.info("redis ready");
|
|
55
|
+
});
|
|
56
|
+
client.on("reconnecting", (delay) => {
|
|
57
|
+
logger?.warn(delay ? `redis reconnecting in ${delay}ms` : "redis reconnecting");
|
|
58
|
+
});
|
|
59
|
+
client.on("error", (err) => {
|
|
60
|
+
logger?.error("redis error", { err: redactError(err) });
|
|
61
|
+
});
|
|
62
|
+
client.on("close", () => {
|
|
63
|
+
logger?.info("redis close");
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/client/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAe,MAAM,cAAc,CAAC;AAC5D,OAAO,EAA6B,MAAM,SAAS,CAAC;AAEpD,0DAA0D;AAC1D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAmBnD,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAI,KAAsB,CAAC,OAAO,CAAC;IAEhD,IACE,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO,KAAK,IAAI;QAChB,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,EACvD,CAAC;QACD,0EAA0E;QAC1E,oEAAoE;QACpE,EAAE;QACF,oEAAoE;QACpE,yEAAyE;QACzE,mDAAmD;QACnD,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,CAAiB,CAAC;QACtD,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;QAElD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;AAEzD,MAAM,UAAU,YAAY,CAAC,MAAmB,EAAE,WAAoB;IACpE,IAAK,MAA6C,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1D,OAAO;IACT,CAAC;IAED,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE;QACnC,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,KAAK;KAClB,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAE5C,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACxB,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACtB,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,KAAa,EAAE,EAAE;QAC1C,MAAM,EAAE,IAAI,CACV,KAAK,CAAC,CAAC,CAAC,yBAAyB,KAAK,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAClE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;QAChC,MAAM,EAAE,KAAK,CAAC,aAAa,EAAE,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACtB,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./client.js";
|
|
2
|
+
export type { RedisClientOptions, Logger } from "./client.js";
|
|
3
|
+
export { attachEvents, redactError } from "./events.js";
|
|
4
|
+
export { health, type HealthResult, type HealthOptions } from "./check.js";
|
|
5
|
+
export { shutdown } from "./shutdown.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,YAAY,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,MAAM,EAAyC,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../../src/client/shutdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAE,KAAK,KAAK,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AA+BpD,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACtC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CA4Df"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { normalizeLogger } from "../logger.js";
|
|
2
|
+
import { redactError } from "./events.js";
|
|
3
|
+
import {} from "ioredis";
|
|
4
|
+
/**
|
|
5
|
+
* How long to wait for QUIT before forcing the connection closed.
|
|
6
|
+
*
|
|
7
|
+
* QUIT is a queued command: ioredis only sends it on a live connection, so a
|
|
8
|
+
* client stuck in `reconnecting` parks it in the offline queue and the promise
|
|
9
|
+
* never settles. Without a bound, `shutdown` hangs for the lifetime of the
|
|
10
|
+
* outage, which is exactly when a process most needs to be able to exit.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
13
|
+
const isClosed = (client) => client.status === "end" || FORCED.has(client);
|
|
14
|
+
/**
|
|
15
|
+
* Clients this function already tore down by force.
|
|
16
|
+
*
|
|
17
|
+
* `disconnect()` only reaches ioredis's `closeHandler` from a live connection.
|
|
18
|
+
* On a client sitting in `reconnecting` the connector has nothing to close, so
|
|
19
|
+
* the status never becomes `end` even though `disconnect()` has cleared the
|
|
20
|
+
* retry timer and the connection can never come back. Shutdown is registered on
|
|
21
|
+
* both SIGINT and SIGTERM in most apps, so the second call would otherwise wait
|
|
22
|
+
* out the full QUIT deadline again for a connection that is already gone.
|
|
23
|
+
*
|
|
24
|
+
* A `WeakSet` keeps this bookkeeping off the client object, so it does not
|
|
25
|
+
* show up in `Object.keys`, in a serialised snapshot, or in BullMQ's own
|
|
26
|
+
* inspection of the connection.
|
|
27
|
+
*/
|
|
28
|
+
const FORCED = new WeakSet();
|
|
29
|
+
export async function shutdown(client, loggerInput) {
|
|
30
|
+
if (!client) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Completing the logger here matters more than anywhere else: shutdown runs
|
|
34
|
+
// during teardown, so `logger?.info is not a function` would replace a clean
|
|
35
|
+
// disconnect with a rejection on the way out.
|
|
36
|
+
const logger = normalizeLogger(loggerInput);
|
|
37
|
+
// Once ioredis reaches "end" the connection is gone for good and QUIT
|
|
38
|
+
// rejects with "Connection is closed.". Shutdown is registered on both
|
|
39
|
+
// SIGINT and SIGTERM in most apps, so a second call has to be a no-op
|
|
40
|
+
// rather than an unhandled rejection during teardown.
|
|
41
|
+
if (isClosed(client)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let timer;
|
|
45
|
+
const expiry = new Promise((resolve) => {
|
|
46
|
+
timer = setTimeout(() => resolve("timeout"), DEFAULT_TIMEOUT_MS);
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const outcome = await Promise.race([
|
|
50
|
+
client.quit().then(() => "quit"),
|
|
51
|
+
expiry,
|
|
52
|
+
]);
|
|
53
|
+
if (outcome === "timeout") {
|
|
54
|
+
// The server never acknowledged QUIT. Tear the connection down
|
|
55
|
+
// directly so the retry loop stops and the process can exit; the
|
|
56
|
+
// caller asked to disconnect, and we are, one way or another.
|
|
57
|
+
logger?.warn(`Redis did not acknowledge QUIT within ${DEFAULT_TIMEOUT_MS}ms, forcing the connection closed`);
|
|
58
|
+
client.disconnect();
|
|
59
|
+
FORCED.add(client);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
logger?.info("Redis disconnected");
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// A connection that died mid-shutdown is already disconnected from the
|
|
66
|
+
// caller's point of view, so treat it as success but still surface it.
|
|
67
|
+
if (isClosed(client)) {
|
|
68
|
+
logger?.warn("Redis connection closed before QUIT completed", redactError(error));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
logger?.error("Failed to disconnect Redis", redactError(error));
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=shutdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shutdown.js","sourceRoot":"","sources":["../../src/client/shutdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAe,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAA6B,MAAM,SAAS,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,MAAM,QAAQ,GAAG,CAAC,MAAmB,EAAW,EAAE,CAChD,MAAM,CAAC,MAAM,KAAK,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,GAAG,IAAI,OAAO,EAAU,CAAC;AAErC,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAsC,EACtC,WAAoB;IAEpB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO;IACT,CAAC;IAED,4EAA4E;IAC5E,6EAA6E;IAC7E,8CAA8C;IAC9C,MAAM,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAE5C,sEAAsE;IACtE,uEAAuE;IACvE,sEAAsE;IACtE,sDAAsD;IACtD,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IAED,IAAI,KAAiC,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;QAChD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YACjC,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAe,CAAC;YACzC,MAAM;SACP,CAAC,CAAC;QAEH,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,+DAA+D;YAC/D,iEAAiE;YACjE,8DAA8D;YAC9D,MAAM,EAAE,IAAI,CACV,yCAAyC,kBAAkB,mCAAmC,CAC/F,CAAC;YACF,MAAM,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAEnB,OAAO;QACT,CAAC;QAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,uEAAuE;QACvE,uEAAuE;QACvE,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACrB,MAAM,EAAE,IAAI,CACV,+CAA+C,EAC/C,WAAW,CAAC,KAAK,CAAC,CACnB,CAAC;YACF,OAAO;QACT,CAAC;QAED,MAAM,EAAE,KAAK,CAAC,4BAA4B,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAEhE,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
error(message: unknown, extra?: unknown): void;
|
|
3
|
+
warn(message: unknown, extra?: unknown): void;
|
|
4
|
+
info(message: unknown, extra?: unknown): void;
|
|
5
|
+
debug(message: unknown, extra?: unknown): void;
|
|
6
|
+
child?(bindings?: Record<string, unknown>): Logger;
|
|
7
|
+
}
|
|
8
|
+
export declare const silentLogger: Logger;
|
|
9
|
+
export declare const consoleLogger: Logger;
|
|
10
|
+
export declare function isLogger(value: unknown): value is Logger;
|
|
11
|
+
/**
|
|
12
|
+
* Complete a caller-supplied logger, or keep "no logger" as no logging.
|
|
13
|
+
*
|
|
14
|
+
* `createLogger` is the public entry point and defaults to the console, which
|
|
15
|
+
* is right when someone asks for a logger. Internal call sites are different:
|
|
16
|
+
* every `createClient(url)` in the wild passes nothing and expects silence, so
|
|
17
|
+
* they need a logger that is either complete or absent. Without this, a logger
|
|
18
|
+
* that implements only some levels throws `logger?.info is not a function` from
|
|
19
|
+
* a connection event, where it is least likely to be caught.
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizeLogger(logger?: Logger | null): Logger | undefined;
|
|
22
|
+
export declare function createLogger(input?: Logger | null): Logger;
|
|
23
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/C,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9C,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9C,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/C,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;CACpD;AAED,eAAO,MAAM,YAAY,EAAE,MAKzB,CAAC;AAEH,eAAO,MAAM,aAAa,EAAE,MAa1B,CAAC;AA8BH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAaxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAM1E;AAED,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAuB1D"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const LOGGER_KEYS = ["error", "warn", "info", "debug"];
|
|
2
|
+
export const silentLogger = Object.freeze({
|
|
3
|
+
error() { },
|
|
4
|
+
warn() { },
|
|
5
|
+
info() { },
|
|
6
|
+
debug() { },
|
|
7
|
+
});
|
|
8
|
+
export const consoleLogger = Object.freeze({
|
|
9
|
+
error(message, extra) {
|
|
10
|
+
write(console.error, message, extra);
|
|
11
|
+
},
|
|
12
|
+
warn(message, extra) {
|
|
13
|
+
write(console.warn, message, extra);
|
|
14
|
+
},
|
|
15
|
+
info(message, extra) {
|
|
16
|
+
write(console.info, message, extra);
|
|
17
|
+
},
|
|
18
|
+
debug(message, extra) {
|
|
19
|
+
write(console.debug, message, extra);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Does this logger take `(bindings, message)` instead of `(message, extra)`?
|
|
24
|
+
*
|
|
25
|
+
* pino and its drop-in forks accept `log.info(bindings, message)`, where the
|
|
26
|
+
* first argument is merged into the record and the second is the message.
|
|
27
|
+
* Those signatures are indistinguishable at the call site, so a caller has to
|
|
28
|
+
* be detected rather than chosen.
|
|
29
|
+
*
|
|
30
|
+
* `child()` was the first thing tried here and it is not a usable signal: this
|
|
31
|
+
* package's own `Logger` interface declares `child?()`, so every conforming
|
|
32
|
+
* logger is allowed to have one. Swapping arguments for those loggers silently
|
|
33
|
+
* moved the message into the bindings slot of every record.
|
|
34
|
+
*
|
|
35
|
+
* A pino instance exposes both `bindings()` and the `levels` map. `child()`
|
|
36
|
+
* loggers inherit `bindings()` from the same prototype, so a caller that hands
|
|
37
|
+
* us `logger.child({ service: "redis" })` is still recognised. Custom loggers
|
|
38
|
+
* have neither.
|
|
39
|
+
*/
|
|
40
|
+
function isBindingsFirst(logger) {
|
|
41
|
+
const candidate = logger;
|
|
42
|
+
return (typeof candidate.bindings === "function" &&
|
|
43
|
+
typeof candidate.levels === "object" &&
|
|
44
|
+
candidate.levels !== null);
|
|
45
|
+
}
|
|
46
|
+
export function isLogger(value) {
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const hasLogFn = LOGGER_KEYS.some((key) => typeof value[key] === "function");
|
|
51
|
+
if (!hasLogFn) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Complete a caller-supplied logger, or keep "no logger" as no logging.
|
|
58
|
+
*
|
|
59
|
+
* `createLogger` is the public entry point and defaults to the console, which
|
|
60
|
+
* is right when someone asks for a logger. Internal call sites are different:
|
|
61
|
+
* every `createClient(url)` in the wild passes nothing and expects silence, so
|
|
62
|
+
* they need a logger that is either complete or absent. Without this, a logger
|
|
63
|
+
* that implements only some levels throws `logger?.info is not a function` from
|
|
64
|
+
* a connection event, where it is least likely to be caught.
|
|
65
|
+
*/
|
|
66
|
+
export function normalizeLogger(logger) {
|
|
67
|
+
if (!logger) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return createLogger(logger);
|
|
71
|
+
}
|
|
72
|
+
export function createLogger(input) {
|
|
73
|
+
if (!input) {
|
|
74
|
+
return consoleLogger;
|
|
75
|
+
}
|
|
76
|
+
if (input === silentLogger || input === consoleLogger) {
|
|
77
|
+
return input;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
error(message, extra) {
|
|
81
|
+
invoke(input, "error", message, extra);
|
|
82
|
+
},
|
|
83
|
+
warn(message, extra) {
|
|
84
|
+
invoke(input, "warn", message, extra);
|
|
85
|
+
},
|
|
86
|
+
info(message, extra) {
|
|
87
|
+
invoke(input, "info", message, extra);
|
|
88
|
+
},
|
|
89
|
+
debug(message, extra) {
|
|
90
|
+
invoke(input, "debug", message, extra);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function write(fn, message, extra) {
|
|
95
|
+
if (extra === undefined) {
|
|
96
|
+
fn(message);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
fn(message, extra);
|
|
100
|
+
}
|
|
101
|
+
function invoke(logger, level, message, extra) {
|
|
102
|
+
const fn = typeof logger[level] === "function"
|
|
103
|
+
? logger[level]
|
|
104
|
+
: typeof logger.info === "function"
|
|
105
|
+
? logger.info
|
|
106
|
+
: undefined;
|
|
107
|
+
if (typeof fn !== "function") {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (extra === undefined) {
|
|
111
|
+
fn.call(logger, message);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (isBindingsFirst(logger)) {
|
|
115
|
+
fn.call(logger, extra, message);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
fn.call(logger, message, extra);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type Redis as RedisClient, type ChainableCommander } from "ioredis";
|
|
2
|
+
import { type Logger } from "../logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* One command in a pipeline, plus a label for reporting failures.
|
|
5
|
+
*
|
|
6
|
+
* The label is what makes a failure identifiable. A pipeline result is a
|
|
7
|
+
* positional array, so a bare `results[7]` tells an operator nothing about which
|
|
8
|
+
* of several hundred commands went wrong; ioredis attaches the command itself to
|
|
9
|
+
* the error, but only for the commands it can describe, and the label is what
|
|
10
|
+
* this package logs.
|
|
11
|
+
*/
|
|
12
|
+
export interface PipelineStep {
|
|
13
|
+
label: string;
|
|
14
|
+
run: (pipeline: ChainableCommander) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface PipelineOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Milliseconds to wait for EXEC before giving up.
|
|
19
|
+
*
|
|
20
|
+
* ioredis queues commands while reconnecting and never flushes them until the
|
|
21
|
+
* connection is back, so EXEC against an unreachable server does not reject —
|
|
22
|
+
* it never settles. Without a bound a request handler awaiting a pipeline
|
|
23
|
+
* hangs for the whole outage and takes the process's request capacity with it.
|
|
24
|
+
*/
|
|
25
|
+
timeout?: number;
|
|
26
|
+
logger?: Logger;
|
|
27
|
+
/**
|
|
28
|
+
* Reject if any single command in the batch failed.
|
|
29
|
+
*
|
|
30
|
+
* Off by default. EXEC resolves even when individual commands fail: each
|
|
31
|
+
* failure arrives as a `[error, null]` tuple in the results, and a pipeline
|
|
32
|
+
* that silently drops one of its writes looks exactly like a pipeline that
|
|
33
|
+
* succeeded. Turn this on where a partial batch is not acceptable.
|
|
34
|
+
*/
|
|
35
|
+
throwOnError?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Result of one command. Mirrors ioredis's own tuple, with the error redacted. */
|
|
38
|
+
export interface PipelineStepResult {
|
|
39
|
+
label: string;
|
|
40
|
+
value: unknown;
|
|
41
|
+
error?: Error;
|
|
42
|
+
}
|
|
43
|
+
export interface PipelineResult {
|
|
44
|
+
results: PipelineStepResult[];
|
|
45
|
+
/** Wall-clock duration of EXEC, in milliseconds. */
|
|
46
|
+
durationMs: number;
|
|
47
|
+
/** How many commands failed. Zero when all succeeded. */
|
|
48
|
+
failed: number;
|
|
49
|
+
}
|
|
50
|
+
export declare class PipelineTimeoutError extends Error {
|
|
51
|
+
readonly steps: number;
|
|
52
|
+
constructor(timeout: number, steps: number);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Thrown when `throwOnError` is set and a command failed.
|
|
56
|
+
*
|
|
57
|
+
* Carries the per-step results so the caller can see which command broke
|
|
58
|
+
* without re-running the batch. The `message` lists labels only — ioredis
|
|
59
|
+
* attaches command arguments to its errors, and a pipeline that batched an
|
|
60
|
+
* `AUTH` would otherwise put the password in an exception message.
|
|
61
|
+
*/
|
|
62
|
+
export declare class PipelineCommandError extends Error {
|
|
63
|
+
readonly results: PipelineStepResult[];
|
|
64
|
+
constructor(results: PipelineStepResult[]);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Thrown when a step does not queue exactly one command.
|
|
68
|
+
*
|
|
69
|
+
* `PipelineStep.run` is typed as returning `void`, and TypeScript allows any
|
|
70
|
+
* value to be returned from a `void` signature — so a step that queues two
|
|
71
|
+
* commands, or an `async` step whose command is only queued after `runPipeline`
|
|
72
|
+
* has already called `exec`, compiles without complaint. Both shift the
|
|
73
|
+
* positional pairing between `steps` and the result tuples, which is the one
|
|
74
|
+
* thing the label exists to prevent, so the batch is rejected rather than
|
|
75
|
+
* returned with a value against the wrong label.
|
|
76
|
+
*/
|
|
77
|
+
export declare class PipelineStepError extends Error {
|
|
78
|
+
readonly label: string;
|
|
79
|
+
readonly queued: number;
|
|
80
|
+
constructor(label: string, queued: number);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run a batch of commands in one round trip.
|
|
84
|
+
*
|
|
85
|
+
* Wraps `client.pipeline()` rather than reimplementing it. The value is in the
|
|
86
|
+
* four sharp edges this closes, all of which are silent in ioredis itself:
|
|
87
|
+
*
|
|
88
|
+
* 1. **A failed command does not fail the pipeline.** EXEC resolves with a
|
|
89
|
+
* `[error, null]` tuple for the command that failed. Code that reads
|
|
90
|
+
* `results.map(([, value]) => value)` gets `null` and carries on as if the
|
|
91
|
+
* write landed. Every result here carries an explicit `error` field.
|
|
92
|
+
* 2. **EXEC can hang forever.** ioredis parks queued commands while
|
|
93
|
+
* reconnecting. `timeout` bounds that.
|
|
94
|
+
* 3. **Command errors carry their arguments.** Redacted via `redactError`
|
|
95
|
+
* before they reach a logger or an exception — on both the per-command
|
|
96
|
+
* results and a rejected `exec()`.
|
|
97
|
+
* 4. **Results are positional.** Each step must queue exactly one command, or
|
|
98
|
+
* every later label is paired with the wrong value. A step that does not
|
|
99
|
+
* raises `PipelineStepError`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function runPipeline(client: RedisClient, steps: PipelineStep[], options?: PipelineOptions): Promise<PipelineResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Values of the successful results, in order.
|
|
104
|
+
*
|
|
105
|
+
* Throws if any command failed, because the alternative is handing back a
|
|
106
|
+
* sparse array whose length matches the batch but whose contents silently
|
|
107
|
+
* include a failed write. Use `results` directly when a partial batch is
|
|
108
|
+
* expected and worth handling.
|
|
109
|
+
*/
|
|
110
|
+
export declare function pipelineValues(results: PipelineStepResult[]): unknown[];
|
|
111
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/pipeline/builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,IAAI,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAG5D;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,mFAAmF;AACnF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAO3C;AAED;;;;;;;GAOG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,OAAO,EAAE,kBAAkB,EAAE,CAAC;gBAE3B,OAAO,EAAE,kBAAkB,EAAE;CAU1C;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAU1C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,YAAY,EAAE,EACrB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAyIzB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE,CAQvE"}
|