ioredis 5.10.1 → 5.11.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 +61 -19
- package/built/Command.d.ts +1 -0
- package/built/Command.js +1 -0
- package/built/Pipeline.js +10 -0
- package/built/Redis.d.ts +11 -0
- package/built/Redis.js +60 -5
- package/built/autoPipelining.js +13 -3
- package/built/index.d.ts +1 -0
- package/built/tracing.d.ts +26 -0
- package/built/tracing.js +96 -0
- package/built/transaction.js +10 -3
- package/built/utils/RedisCommander.d.ts +779 -1
- package/built/utils/defaults.d.ts +6 -0
- package/built/utils/defaults.js +102 -0
- package/built/utils/index.d.ts +2 -2
- package/built/utils/index.js +57 -21
- package/built/utils/isArguments.d.ts +1 -0
- package/built/utils/isArguments.js +48 -0
- package/built/utils/lodash.d.ts +2 -3
- package/built/utils/lodash.js +13 -5
- package/package.json +9 -13
package/README.md
CHANGED
|
@@ -729,13 +729,16 @@ const stream = redis.zscanStream("myhash", {
|
|
|
729
729
|
match: "age:??",
|
|
730
730
|
});
|
|
731
731
|
```
|
|
732
|
+
|
|
732
733
|
The `hscanStream` also accepts the `noValues` option to specify whether Redis should return only the keys in the hash table without their corresponding values.
|
|
734
|
+
|
|
733
735
|
```javascript
|
|
734
736
|
const stream = redis.hscanStream("myhash", {
|
|
735
737
|
match: "age:??",
|
|
736
738
|
noValues: true,
|
|
737
739
|
});
|
|
738
740
|
```
|
|
741
|
+
|
|
739
742
|
You can learn more from the [Redis documentation](http://redis.io/commands/scan).
|
|
740
743
|
|
|
741
744
|
**Useful Tips**
|
|
@@ -815,6 +818,7 @@ const redis = new Redis({
|
|
|
815
818
|
```
|
|
816
819
|
|
|
817
820
|
When enabled:
|
|
821
|
+
|
|
818
822
|
- For commands with a finite timeout (e.g., `blpop("key", 5)`), ioredis sets a client-side deadline based on the command's timeout plus a small grace period (`blockingTimeoutGrace`, default 100ms). If no reply arrives before the deadline, the command resolves with `null`—the same value Redis returns when a blocking command times out normally.
|
|
819
823
|
- For commands that block forever (e.g., `timeout = 0` or `BLOCK 0`), the `blockingTimeout` value is used as a safety net.
|
|
820
824
|
|
|
@@ -842,15 +846,15 @@ On ElastiCache instances with Auto-failover enabled, `reconnectOnError` does not
|
|
|
842
846
|
|
|
843
847
|
The Redis instance will emit some events about the state of the connection to the Redis server.
|
|
844
848
|
|
|
845
|
-
| Event | Description
|
|
846
|
-
| :----------- |
|
|
847
|
-
| connect | emits when a connection is established to the Redis server.
|
|
848
|
-
| ready | If `enableReadyCheck` is `true`, client will emit `ready` when the server reports that it is ready to receive commands (e.g. finish loading data from disk).<br>Otherwise, `ready` will be emitted immediately right after the `connect` event.
|
|
849
|
-
| error | emits when an error occurs while connecting.<br>However, ioredis emits all `error` events silently (only emits when there's at least one listener) so that your application won't crash if you're not listening to the `error` event.<br>When `redis.connect()` is explicitly called the error will also be rejected from the returned promise, in addition to emitting it. If `redis.connect()` is not called explicitly and `lazyConnect` is true, ioredis will try to connect automatically on the first command and emit the `error` event silently.
|
|
850
|
-
| close | emits when an established Redis server connection has closed.
|
|
851
|
-
| reconnecting | emits after `close` when a reconnection will be made. The argument of the event is the time (in ms) before reconnecting.
|
|
852
|
-
| end | emits after `close` when no more reconnections will be made, or the connection is failed to establish.
|
|
853
|
-
| wait | emits when `lazyConnect` is set and will wait for the first command to be called before connecting.
|
|
849
|
+
| Event | Description |
|
|
850
|
+
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
851
|
+
| connect | emits when a connection is established to the Redis server. |
|
|
852
|
+
| ready | If `enableReadyCheck` is `true`, client will emit `ready` when the server reports that it is ready to receive commands (e.g. finish loading data from disk).<br>Otherwise, `ready` will be emitted immediately right after the `connect` event. |
|
|
853
|
+
| error | emits when an error occurs while connecting.<br>However, ioredis emits all `error` events silently (only emits when there's at least one listener) so that your application won't crash if you're not listening to the `error` event.<br>When `redis.connect()` is explicitly called the error will also be rejected from the returned promise, in addition to emitting it. If `redis.connect()` is not called explicitly and `lazyConnect` is true, ioredis will try to connect automatically on the first command and emit the `error` event silently. |
|
|
854
|
+
| close | emits when an established Redis server connection has closed. |
|
|
855
|
+
| reconnecting | emits after `close` when a reconnection will be made. The argument of the event is the time (in ms) before reconnecting. |
|
|
856
|
+
| end | emits after `close` when no more reconnections will be made, or the connection is failed to establish. |
|
|
857
|
+
| wait | emits when `lazyConnect` is set and will wait for the first command to be called before connecting. |
|
|
854
858
|
|
|
855
859
|
You can also check out the `Redis#status` property to get the current connection status.
|
|
856
860
|
|
|
@@ -860,6 +864,38 @@ Besides the above connection events, there are several other custom events:
|
|
|
860
864
|
| :----- | :------------------------------------------------------------------ |
|
|
861
865
|
| select | emits when the database changed. The argument is the new db number. |
|
|
862
866
|
|
|
867
|
+
## Diagnostics Channel
|
|
868
|
+
|
|
869
|
+
ioredis publishes telemetry through Node.js [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html), allowing APM tools and custom instrumentation to observe commands, connections, and batch operations without modifying application code.
|
|
870
|
+
|
|
871
|
+
These channels use `TracingChannel#tracePromise()` and emit `start`, `end`, `asyncStart`, `asyncEnd`, and `error` sub-events. Requires Node.js >= 18.19.0; on older versions the channels are silently unavailable (zero overhead). Subscribe via `tracing:<name>:<event>`:
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
import dc from "node:diagnostics_channel";
|
|
875
|
+
|
|
876
|
+
dc.subscribe("tracing:ioredis:command:start", ({ command, args }) => {
|
|
877
|
+
console.log(`> ${command}`, args);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
dc.subscribe("tracing:ioredis:command:asyncEnd", ({ command }) => {
|
|
881
|
+
console.log(`${command} settled`);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
dc.subscribe("tracing:ioredis:command:error", ({ command, error }) => {
|
|
885
|
+
console.error(`${command} failed:`, error);
|
|
886
|
+
});
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
| Channel name | Payload | Description |
|
|
890
|
+
| :---------------- | :---------------------- | :------------------------------------------------------- |
|
|
891
|
+
| `ioredis:command` | `CommandTraceContext` | Individual command (standalone, pipeline, or within MULTI) |
|
|
892
|
+
| `ioredis:batch` | `BatchOperationContext` | MULTI transaction as a whole |
|
|
893
|
+
| `ioredis:connect` | `ConnectTraceContext` | Socket connection attempt |
|
|
894
|
+
|
|
895
|
+
Command arguments are sanitized before emission using rules adapted from `@opentelemetry/redis-common`. Sensitive values (e.g. values in `SET`, `AUTH` passwords) are replaced with `?`, while read-only commands like `GET` and `DEL` retain all arguments. This is safe by default: unlisted or custom commands have all arguments redacted.
|
|
896
|
+
|
|
897
|
+
Context types (`CommandTraceContext`, `BatchOperationContext`, `ConnectTraceContext`) are exported from `ioredis`.
|
|
898
|
+
|
|
863
899
|
## Offline Queue
|
|
864
900
|
|
|
865
901
|
When a command can't be processed by Redis (being sent before the `ready` event), by default, it's added to the offline queue and will be
|
|
@@ -1159,6 +1195,7 @@ const cluster = new Redis.Cluster(
|
|
|
1159
1195
|
```
|
|
1160
1196
|
|
|
1161
1197
|
Or you can specify this parameter through function:
|
|
1198
|
+
|
|
1162
1199
|
```javascript
|
|
1163
1200
|
const cluster = new Redis.Cluster(
|
|
1164
1201
|
[
|
|
@@ -1169,7 +1206,7 @@ const cluster = new Redis.Cluster(
|
|
|
1169
1206
|
],
|
|
1170
1207
|
{
|
|
1171
1208
|
natMap: (key) => {
|
|
1172
|
-
if(key.includes(
|
|
1209
|
+
if (key.includes("30001")) {
|
|
1173
1210
|
return { host: "203.0.113.73", port: 30001 };
|
|
1174
1211
|
}
|
|
1175
1212
|
|
|
@@ -1226,28 +1263,33 @@ For sharded Pub/Sub, use the `spublish` and `ssubscribe` commands instead of the
|
|
|
1226
1263
|
The following basic example shows you how to use sharded Pub/Sub:
|
|
1227
1264
|
|
|
1228
1265
|
```javascript
|
|
1229
|
-
const cluster: Cluster = new Cluster([{host: host, port: port}], {
|
|
1266
|
+
const cluster: Cluster = new Cluster([{ host: host, port: port }], {
|
|
1267
|
+
shardedSubscribers: true,
|
|
1268
|
+
});
|
|
1230
1269
|
|
|
1231
1270
|
//Register the callback
|
|
1232
1271
|
cluster.on("smessage", (channel, message) => {
|
|
1233
|
-
|
|
1272
|
+
console.log(message);
|
|
1234
1273
|
});
|
|
1235
1274
|
|
|
1236
|
-
|
|
1237
1275
|
//Subscribe to the channels on the same slot
|
|
1238
|
-
cluster
|
|
1276
|
+
cluster
|
|
1277
|
+
.ssubscribe("channel{my}:1", "channel{my}:2")
|
|
1278
|
+
.then((count: number) => {
|
|
1239
1279
|
console.log(count);
|
|
1240
|
-
})
|
|
1280
|
+
})
|
|
1281
|
+
.catch((err) => {
|
|
1241
1282
|
console.log(err);
|
|
1242
|
-
});
|
|
1283
|
+
});
|
|
1243
1284
|
|
|
1244
1285
|
//Publish a message
|
|
1245
|
-
cluster
|
|
1286
|
+
cluster
|
|
1287
|
+
.spublish("channel{my}:1", "This is a test message to my first channel.")
|
|
1288
|
+
.then((value: number) => {
|
|
1246
1289
|
console.log("Published a message to channel{my}:1");
|
|
1247
|
-
});
|
|
1290
|
+
});
|
|
1248
1291
|
```
|
|
1249
1292
|
|
|
1250
|
-
|
|
1251
1293
|
### Events
|
|
1252
1294
|
|
|
1253
1295
|
| Event | Description |
|
package/built/Command.d.ts
CHANGED
package/built/Command.js
CHANGED
package/built/Pipeline.js
CHANGED
|
@@ -5,6 +5,7 @@ const commands_1 = require("@ioredis/commands");
|
|
|
5
5
|
const standard_as_callback_1 = require("standard-as-callback");
|
|
6
6
|
const util_1 = require("util");
|
|
7
7
|
const Command_1 = require("./Command");
|
|
8
|
+
const buffer_1 = require("buffer");
|
|
8
9
|
const utils_1 = require("./utils");
|
|
9
10
|
const Commander_1 = require("./utils/Commander");
|
|
10
11
|
/*
|
|
@@ -315,6 +316,15 @@ Pipeline.prototype.exec = function (callback) {
|
|
|
315
316
|
buffers.push(writable);
|
|
316
317
|
}
|
|
317
318
|
else {
|
|
319
|
+
if (data.length + writable.length >= buffer_1.constants.MAX_STRING_LENGTH) {
|
|
320
|
+
if (!buffers) {
|
|
321
|
+
buffers = [];
|
|
322
|
+
}
|
|
323
|
+
if (data) {
|
|
324
|
+
buffers.push(Buffer.from(data, "utf8"));
|
|
325
|
+
data = "";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
318
328
|
data += writable;
|
|
319
329
|
}
|
|
320
330
|
if (!--writePending) {
|
package/built/Redis.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { RedisOptions } from "./redis/RedisOptions";
|
|
|
7
7
|
import ScanStream from "./ScanStream";
|
|
8
8
|
import { Transaction } from "./transaction";
|
|
9
9
|
import { Callback, CommandItem, NetStream, ScanStreamOptions, WriteableStream } from "./types";
|
|
10
|
+
import { type BatchOperationContext } from "./tracing";
|
|
10
11
|
import Commander from "./utils/Commander";
|
|
11
12
|
import Deque = require("denque");
|
|
12
13
|
declare type RedisStatus = "wait" | "reconnecting" | "connecting" | "connect" | "ready" | "close" | "end";
|
|
@@ -85,6 +86,7 @@ declare class Redis extends Commander implements DataHandledable {
|
|
|
85
86
|
* if the connection fails, times out, or if Redis is already connecting/connected.
|
|
86
87
|
*/
|
|
87
88
|
connect(callback?: Callback<void>): Promise<void>;
|
|
89
|
+
private _connect;
|
|
88
90
|
/**
|
|
89
91
|
* Disconnect from Redis.
|
|
90
92
|
*
|
|
@@ -186,6 +188,15 @@ declare class Redis extends Commander implements DataHandledable {
|
|
|
186
188
|
* @ignore
|
|
187
189
|
*/
|
|
188
190
|
handleReconnection(err: Error, item: CommandItem): void;
|
|
191
|
+
/**
|
|
192
|
+
* @ignore
|
|
193
|
+
*/
|
|
194
|
+
_getServerAddress(): {
|
|
195
|
+
address: string;
|
|
196
|
+
port: number | undefined;
|
|
197
|
+
};
|
|
198
|
+
private _buildCommandContext;
|
|
199
|
+
_buildBatchContext(batchSize: number): BatchOperationContext;
|
|
189
200
|
/**
|
|
190
201
|
* Get description of the connection. Used for debugging.
|
|
191
202
|
*/
|
package/built/Redis.js
CHANGED
|
@@ -12,6 +12,7 @@ const RedisOptions_1 = require("./redis/RedisOptions");
|
|
|
12
12
|
const ScanStream_1 = require("./ScanStream");
|
|
13
13
|
const transaction_1 = require("./transaction");
|
|
14
14
|
const utils_1 = require("./utils");
|
|
15
|
+
const tracing_1 = require("./tracing");
|
|
15
16
|
const applyMixin_1 = require("./utils/applyMixin");
|
|
16
17
|
const Commander_1 = require("./utils/Commander");
|
|
17
18
|
const lodash_1 = require("./utils/lodash");
|
|
@@ -101,7 +102,18 @@ class Redis extends Commander_1.default {
|
|
|
101
102
|
* if the connection fails, times out, or if Redis is already connecting/connected.
|
|
102
103
|
*/
|
|
103
104
|
connect(callback) {
|
|
104
|
-
const promise =
|
|
105
|
+
const promise = (0, tracing_1.traceConnect)(() => this._connect(), () => {
|
|
106
|
+
const { address, port } = this._getServerAddress();
|
|
107
|
+
return {
|
|
108
|
+
serverAddress: address,
|
|
109
|
+
serverPort: port,
|
|
110
|
+
connectionEpoch: this.connectionEpoch,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return (0, standard_as_callback_1.default)(promise, callback);
|
|
114
|
+
}
|
|
115
|
+
_connect() {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
105
117
|
if (this.status === "connecting" ||
|
|
106
118
|
this.status === "connect" ||
|
|
107
119
|
this.status === "ready") {
|
|
@@ -211,7 +223,6 @@ class Redis extends Commander_1.default {
|
|
|
211
223
|
_this.once("close", connectionCloseHandler);
|
|
212
224
|
});
|
|
213
225
|
});
|
|
214
|
-
return (0, standard_as_callback_1.default)(promise, callback);
|
|
215
226
|
}
|
|
216
227
|
/**
|
|
217
228
|
* Disconnect from Redis.
|
|
@@ -416,7 +427,8 @@ class Redis extends Commander_1.default {
|
|
|
416
427
|
if (Command_1.default.checkFlag("WILL_DISCONNECT", command.name)) {
|
|
417
428
|
this.manuallyClosing = true;
|
|
418
429
|
}
|
|
419
|
-
if (this.options.socketTimeout !== undefined &&
|
|
430
|
+
if (this.options.socketTimeout !== undefined &&
|
|
431
|
+
this.socketTimeoutTimer === undefined) {
|
|
420
432
|
this.setSocketTimeout();
|
|
421
433
|
}
|
|
422
434
|
}
|
|
@@ -428,7 +440,15 @@ class Redis extends Commander_1.default {
|
|
|
428
440
|
debug("switch to db [%d]", this.condition.select);
|
|
429
441
|
}
|
|
430
442
|
}
|
|
431
|
-
|
|
443
|
+
if (!writable || command.isTraced) {
|
|
444
|
+
return command.promise;
|
|
445
|
+
}
|
|
446
|
+
// Trace on the write path only, and only once per command. Commands may
|
|
447
|
+
// pass through sendCommand multiple times (offline queue flush,
|
|
448
|
+
// prevCommandQueue resend after reconnect). The isTraced flag ensures
|
|
449
|
+
// we don't emit duplicate trace events.
|
|
450
|
+
command.isTraced = true;
|
|
451
|
+
return (0, tracing_1.traceCommand)(() => command.promise, () => this._buildCommandContext(command));
|
|
432
452
|
}
|
|
433
453
|
getBlockingTimeoutInMs(command) {
|
|
434
454
|
var _a;
|
|
@@ -444,7 +464,8 @@ class Redis extends Commander_1.default {
|
|
|
444
464
|
if (typeof timeout === "number") {
|
|
445
465
|
if (timeout > 0) {
|
|
446
466
|
// Finite timeout from command args - add grace period
|
|
447
|
-
return timeout +
|
|
467
|
+
return (timeout +
|
|
468
|
+
((_a = this.options.blockingTimeoutGrace) !== null && _a !== void 0 ? _a : RedisOptions_1.DEFAULT_REDIS_OPTIONS.blockingTimeoutGrace));
|
|
448
469
|
}
|
|
449
470
|
// Command has timeout=0 (block forever), use blockingTimeout option as safety net
|
|
450
471
|
return configuredTimeout;
|
|
@@ -575,6 +596,40 @@ class Redis extends Commander_1.default {
|
|
|
575
596
|
item.command.reject(err);
|
|
576
597
|
}
|
|
577
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* @ignore
|
|
601
|
+
*/
|
|
602
|
+
_getServerAddress() {
|
|
603
|
+
if ("path" in this.options && this.options.path) {
|
|
604
|
+
return { address: this.options.path, port: undefined };
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
address: ("host" in this.options && this.options.host) || "localhost",
|
|
608
|
+
port: ("port" in this.options && this.options.port) || 6379,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
_buildCommandContext(command) {
|
|
612
|
+
var _a, _b, _c;
|
|
613
|
+
const { address, port } = this._getServerAddress();
|
|
614
|
+
return {
|
|
615
|
+
command: command.name,
|
|
616
|
+
args: (0, tracing_1.sanitizeArgs)(command.name, command.args),
|
|
617
|
+
database: (_c = (_b = (_a = this.condition) === null || _a === void 0 ? void 0 : _a.select) !== null && _b !== void 0 ? _b : this.options.db) !== null && _c !== void 0 ? _c : 0,
|
|
618
|
+
serverAddress: address,
|
|
619
|
+
serverPort: port,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
_buildBatchContext(batchSize) {
|
|
623
|
+
var _a, _b, _c;
|
|
624
|
+
const { address, port } = this._getServerAddress();
|
|
625
|
+
return {
|
|
626
|
+
batchMode: "MULTI",
|
|
627
|
+
batchSize,
|
|
628
|
+
database: (_c = (_b = (_a = this.condition) === null || _a === void 0 ? void 0 : _a.select) !== null && _b !== void 0 ? _b : this.options.db) !== null && _c !== void 0 ? _c : 0,
|
|
629
|
+
serverAddress: address,
|
|
630
|
+
serverPort: port,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
578
633
|
/**
|
|
579
634
|
* Get description of the connection. Used for debugging.
|
|
580
635
|
*/
|
package/built/autoPipelining.js
CHANGED
|
@@ -100,6 +100,18 @@ function getFirstValueInFlattenedArray(args) {
|
|
|
100
100
|
return undefined;
|
|
101
101
|
}
|
|
102
102
|
exports.getFirstValueInFlattenedArray = getFirstValueInFlattenedArray;
|
|
103
|
+
function getFirstKeyForCommand(commandName, args) {
|
|
104
|
+
if ((0, commands_1.exists)(commandName, { caseInsensitive: true })) {
|
|
105
|
+
const flattenedArgs = args.flat();
|
|
106
|
+
const keyIndexes = (0, commands_1.getKeyIndexes)(commandName, flattenedArgs, {
|
|
107
|
+
nameCaseInsensitive: true,
|
|
108
|
+
});
|
|
109
|
+
if (keyIndexes.length) {
|
|
110
|
+
return flattenedArgs[keyIndexes[0]];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return getFirstValueInFlattenedArray(args);
|
|
114
|
+
}
|
|
103
115
|
function executeWithAutoPipelining(client, functionName, commandName, args, callback) {
|
|
104
116
|
// On cluster mode let's wait for slots to be available
|
|
105
117
|
if (client.isCluster && !client.slots.length) {
|
|
@@ -116,11 +128,9 @@ function executeWithAutoPipelining(client, functionName, commandName, args, call
|
|
|
116
128
|
}), callback);
|
|
117
129
|
}
|
|
118
130
|
// If we have slot information, we can improve routing by grouping slots served by the same subset of nodes
|
|
119
|
-
// Note that the first value in args may be a (possibly empty) array.
|
|
120
|
-
// ioredis will only flatten one level of the array, in the Command constructor.
|
|
121
131
|
const prefix = client.options.keyPrefix || "";
|
|
122
132
|
let slotKey = client.isCluster
|
|
123
|
-
? client.slots[calculateSlot(`${prefix}${
|
|
133
|
+
? client.slots[calculateSlot(`${prefix}${getFirstKeyForCommand(commandName, args)}`)].join(",")
|
|
124
134
|
: "main";
|
|
125
135
|
// When scaleReads is enabled, separate read and write commands into different pipelines
|
|
126
136
|
// so they can be routed to replicas and masters respectively
|
package/built/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { ClusterNode } from "./cluster";
|
|
|
36
36
|
export { ClusterOptions, DNSLookupFunction, DNSResolveSrvFunction, NatMap, } from "./cluster/ClusterOptions";
|
|
37
37
|
export { NodeRole } from "./cluster/util";
|
|
38
38
|
export type { RedisKey, RedisValue, ChainableCommander, } from "./utils/RedisCommander";
|
|
39
|
+
export type { CommandTraceContext, BatchOperationContext, ConnectTraceContext, } from "./tracing";
|
|
39
40
|
export declare const ReplyError: any;
|
|
40
41
|
/**
|
|
41
42
|
* @ignore
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CommandParameter } from "./types";
|
|
2
|
+
export declare function sanitizeArgs(commandName: string, args: CommandParameter[]): string[];
|
|
3
|
+
export interface CommandTraceContext {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
database: number;
|
|
7
|
+
serverAddress: string;
|
|
8
|
+
serverPort: number | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface BatchOperationContext {
|
|
11
|
+
batchMode: "MULTI";
|
|
12
|
+
batchSize: number;
|
|
13
|
+
database: number;
|
|
14
|
+
serverAddress: string;
|
|
15
|
+
serverPort: number | undefined;
|
|
16
|
+
}
|
|
17
|
+
export interface ConnectTraceContext {
|
|
18
|
+
serverAddress: string;
|
|
19
|
+
serverPort: number | undefined;
|
|
20
|
+
connectionEpoch: number;
|
|
21
|
+
}
|
|
22
|
+
declare type CommandContext = CommandTraceContext;
|
|
23
|
+
export declare function traceCommand<T>(fn: () => Promise<T>, contextFactory: () => CommandContext): Promise<T>;
|
|
24
|
+
export declare function traceBatch<T>(fn: () => Promise<T>, contextFactory: () => BatchOperationContext): Promise<T>;
|
|
25
|
+
export declare function traceConnect<T>(fn: () => Promise<T>, contextFactory: () => ConnectTraceContext): Promise<T>;
|
|
26
|
+
export {};
|
package/built/tracing.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.traceConnect = exports.traceBatch = exports.traceCommand = exports.sanitizeArgs = void 0;
|
|
4
|
+
// Argument sanitization rules adapted from @opentelemetry/redis-common (Apache 2.0).
|
|
5
|
+
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/packages/redis-common/src/index.ts
|
|
6
|
+
//
|
|
7
|
+
// Each entry specifies how many positional args (after the command name) are safe
|
|
8
|
+
// to emit. -1 means all args are safe (read-only/structural commands).
|
|
9
|
+
// Unlisted commands default to 0 (all args redacted) for safe-by-default behavior,
|
|
10
|
+
// which covers AUTH, HELLO, and unknown/custom commands.
|
|
11
|
+
const SERIALIZATION_SUBSETS = [
|
|
12
|
+
{ regex: /^ECHO/i, args: 0 },
|
|
13
|
+
{
|
|
14
|
+
regex: /^(LPUSH|MSET|PFA|PUBLISH|RPUSH|SADD|SET|SPUBLISH|XADD|ZADD)/i,
|
|
15
|
+
args: 1,
|
|
16
|
+
},
|
|
17
|
+
{ regex: /^(HSET|HMSET|LSET|LINSERT)/i, args: 2 },
|
|
18
|
+
{
|
|
19
|
+
regex: /^(ACL|BIT|B[LRZ]|CLIENT|CLUSTER|CONFIG|COMMAND|DECR|DEL|EVAL|EX|FUNCTION|GEO|GET|HINCR|HMGET|HSCAN|INCR|L[TRLM]|MEMORY|P[EFISTU]|RPOP|S[CDIMORSU]|XACK|X[CDGILPRT]|Z[CDILMPRS])/i,
|
|
20
|
+
args: -1,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
function sanitizeArgs(commandName, args) {
|
|
24
|
+
let allowedArgCount = 0;
|
|
25
|
+
for (const subset of SERIALIZATION_SUBSETS) {
|
|
26
|
+
if (subset.regex.test(commandName)) {
|
|
27
|
+
allowedArgCount = subset.args;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (allowedArgCount === -1) {
|
|
32
|
+
return args.map((a) => String(a));
|
|
33
|
+
}
|
|
34
|
+
const result = [];
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
if (i < allowedArgCount) {
|
|
37
|
+
result.push(String(args[i]));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
result.push("?");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
exports.sanitizeArgs = sanitizeArgs;
|
|
46
|
+
// Load diagnostics_channel with Node 18 compatibility
|
|
47
|
+
const dc = (() => {
|
|
48
|
+
try {
|
|
49
|
+
return "getBuiltinModule" in process
|
|
50
|
+
? process.getBuiltinModule("node:diagnostics_channel")
|
|
51
|
+
: require("node:diagnostics_channel");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
const hasTracingChannel = dc && typeof dc.tracingChannel === "function";
|
|
58
|
+
const commandChannel = hasTracingChannel
|
|
59
|
+
? dc.tracingChannel("ioredis:command")
|
|
60
|
+
: undefined;
|
|
61
|
+
const batchChannel = hasTracingChannel
|
|
62
|
+
? dc.tracingChannel("ioredis:batch")
|
|
63
|
+
: undefined;
|
|
64
|
+
const connectChannel = hasTracingChannel
|
|
65
|
+
? dc.tracingChannel("ioredis:connect")
|
|
66
|
+
: undefined;
|
|
67
|
+
function shouldTrace(channel) {
|
|
68
|
+
return !!channel && channel.hasSubscribers !== false;
|
|
69
|
+
}
|
|
70
|
+
const noop = () => { };
|
|
71
|
+
function traceCommand(fn, contextFactory) {
|
|
72
|
+
if (!shouldTrace(commandChannel))
|
|
73
|
+
return fn();
|
|
74
|
+
// tracePromise returns a wrapper promise that re-rejects on error.
|
|
75
|
+
// Silence the wrapper to prevent unhandled rejections when callers
|
|
76
|
+
// (e.g. Pipeline) discard the return value. Callers that await this
|
|
77
|
+
// promise still see the rejection through their own .then() chain.
|
|
78
|
+
const traced = commandChannel.tracePromise(fn, contextFactory());
|
|
79
|
+
traced.catch(noop);
|
|
80
|
+
return traced;
|
|
81
|
+
}
|
|
82
|
+
exports.traceCommand = traceCommand;
|
|
83
|
+
function traceBatch(fn, contextFactory) {
|
|
84
|
+
if (!shouldTrace(batchChannel))
|
|
85
|
+
return fn();
|
|
86
|
+
const traced = batchChannel.tracePromise(fn, contextFactory());
|
|
87
|
+
traced.catch(noop);
|
|
88
|
+
return traced;
|
|
89
|
+
}
|
|
90
|
+
exports.traceBatch = traceBatch;
|
|
91
|
+
function traceConnect(fn, contextFactory) {
|
|
92
|
+
if (!shouldTrace(connectChannel))
|
|
93
|
+
return fn();
|
|
94
|
+
return connectChannel.tracePromise(fn, contextFactory());
|
|
95
|
+
}
|
|
96
|
+
exports.traceConnect = traceConnect;
|
package/built/transaction.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.addTransactionSupport = void 0;
|
|
|
4
4
|
const utils_1 = require("./utils");
|
|
5
5
|
const standard_as_callback_1 = require("standard-as-callback");
|
|
6
6
|
const Pipeline_1 = require("./Pipeline");
|
|
7
|
+
const tracing_1 = require("./tracing");
|
|
7
8
|
function addTransactionSupport(redis) {
|
|
8
9
|
redis.pipeline = function (commands) {
|
|
9
10
|
const pipeline = new Pipeline_1.default(this);
|
|
@@ -51,8 +52,9 @@ function addTransactionSupport(redis) {
|
|
|
51
52
|
if (this.nodeifiedPromise) {
|
|
52
53
|
return exec.call(pipeline);
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// Count only user commands (exclude MULTI/EXEC wrappers).
|
|
56
|
+
const batchSize = Math.max(pipeline.length - 2, 0);
|
|
57
|
+
const execAndUnwrap = () => exec.call(pipeline).then(function (result) {
|
|
56
58
|
const execResult = result[result.length - 1];
|
|
57
59
|
if (typeof execResult === "undefined") {
|
|
58
60
|
throw new Error("Pipeline cannot be used to send any commands when the `exec()` has been called on it.");
|
|
@@ -67,7 +69,12 @@ function addTransactionSupport(redis) {
|
|
|
67
69
|
throw execResult[0];
|
|
68
70
|
}
|
|
69
71
|
return (0, utils_1.wrapMultiResult)(execResult[1]);
|
|
70
|
-
})
|
|
72
|
+
});
|
|
73
|
+
// Trace MULTI as a single batch operation on ioredis:batch.
|
|
74
|
+
const promise = "_buildBatchContext" in this.redis
|
|
75
|
+
? (0, tracing_1.traceBatch)(execAndUnwrap, () => this.redis._buildBatchContext(batchSize))
|
|
76
|
+
: execAndUnwrap();
|
|
77
|
+
return (0, standard_as_callback_1.default)(promise, callback);
|
|
71
78
|
};
|
|
72
79
|
// @ts-expect-error
|
|
73
80
|
const { execBuffer } = pipeline;
|