Review funkt
This commit is contained in:
373
node_modules/ioredis/built/DataHandler.js
generated
vendored
Normal file
373
node_modules/ioredis/built/DataHandler.js
generated
vendored
Normal file
@@ -0,0 +1,373 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Command_1 = require("./Command");
|
||||
const utils_1 = require("./utils");
|
||||
const SubscriptionSet_1 = require("./SubscriptionSet");
|
||||
const decoder_1 = require("./resp/decoder");
|
||||
const debug = (0, utils_1.Debug)("dataHandler");
|
||||
class DataHandler {
|
||||
constructor(redis, parserOptions) {
|
||||
this.redis = redis;
|
||||
// Parser options can't change over the lifetime of a connection, so the
|
||||
// mapping is resolved once instead of per reply.
|
||||
const typeMapping = getParserTypeMapping(parserOptions);
|
||||
const decoder = new decoder_1.Decoder({
|
||||
getTypeMapping: () => typeMapping,
|
||||
onReply: (reply) => {
|
||||
this.dispatch(() => this.returnReply(reply));
|
||||
},
|
||||
onErrorReply: (err) => {
|
||||
this.dispatch(() => this.returnError(err));
|
||||
},
|
||||
onPush: (reply) => {
|
||||
this.dispatch(() => this.returnPush(reply));
|
||||
},
|
||||
});
|
||||
// prependListener ensures the parser receives and processes data before socket timeout checks are performed
|
||||
redis.stream.prependListener("data", (data) => {
|
||||
try {
|
||||
decoder.write(data);
|
||||
}
|
||||
catch (err) {
|
||||
this.returnFatalError(err);
|
||||
}
|
||||
});
|
||||
// prependListener() doesn't enable flowing mode automatically - we need to resume the stream manually
|
||||
redis.stream.resume();
|
||||
}
|
||||
// Rethrows user listener/transformer exceptions asynchronously so they
|
||||
// surface as uncaught exceptions (as with redis-parser in v5) instead of
|
||||
// unwinding the decoder mid-parse and tearing down the connection as a
|
||||
// fatal protocol error.
|
||||
dispatch(fn) {
|
||||
try {
|
||||
fn();
|
||||
}
|
||||
catch (err) {
|
||||
process.nextTick(() => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
returnFatalError(err) {
|
||||
err.message += ". Please report this.";
|
||||
this.redis.recoverFromFatalError(err, err, { offlineQueue: false });
|
||||
}
|
||||
returnError(err) {
|
||||
const item = this.shiftCommand(err);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
err.command = {
|
||||
name: item.command.name,
|
||||
args: item.command.args,
|
||||
};
|
||||
// A MOVED reply to SSUBSCRIBE means the shard channel's slot migrated.
|
||||
// Signal the subscriber layer to refresh its topology, but still settle
|
||||
// the command through normal error handling so the caller's promise
|
||||
// rejects instead of staying pending forever.
|
||||
const isMovedSsubscribe = item.command.name === "ssubscribe" && err.message.startsWith("MOVED ");
|
||||
if (isMovedSsubscribe) {
|
||||
this.redis.emit("moved");
|
||||
}
|
||||
this.redis.handleReconnection(err, item);
|
||||
}
|
||||
returnReply(reply) {
|
||||
if (this.handleMonitorReply(reply)) {
|
||||
return;
|
||||
}
|
||||
// Under RESP3, pub/sub traffic arrives exclusively as push frames
|
||||
// (returnPush), so a normal reply on a subscribed connection is always a
|
||||
// command response. Routing it by content would misinterpret replies that
|
||||
// merely look like pub/sub messages (e.g. an LRANGE result starting with
|
||||
// "message") and desync the command queue.
|
||||
if (this.redis.condition.protocol !== 3 && this.handleSubscriberReply(reply)) {
|
||||
return;
|
||||
}
|
||||
const item = this.shiftCommand(reply);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (Command_1.default.checkFlag("ENTER_SUBSCRIBER_MODE", item.command.name)) {
|
||||
this.redis.condition.subscriber = new SubscriptionSet_1.default();
|
||||
this.redis.condition.subscriber.add(item.command.name, reply[1].toString());
|
||||
if (!fillSubCommand(item.command, reply[2])) {
|
||||
this.redis.commandQueue.unshift(item);
|
||||
}
|
||||
}
|
||||
else if (Command_1.default.checkFlag("EXIT_SUBSCRIBER_MODE", item.command.name)) {
|
||||
if (!fillUnsubCommand(item.command, reply[2])) {
|
||||
this.redis.commandQueue.unshift(item);
|
||||
}
|
||||
}
|
||||
else {
|
||||
item.command.resolve(reply);
|
||||
}
|
||||
}
|
||||
returnPush(reply) {
|
||||
if (!Array.isArray(reply) || reply.length === 0) {
|
||||
return;
|
||||
}
|
||||
const replyType = reply[0].toString();
|
||||
debug('receive push "%s"', replyType);
|
||||
switch (replyType) {
|
||||
case "message":
|
||||
case "pmessage":
|
||||
case "smessage":
|
||||
this.handleSubscriberReply(reply);
|
||||
break;
|
||||
case "ssubscribe":
|
||||
case "subscribe":
|
||||
case "psubscribe": {
|
||||
if (!this.redis.condition.subscriber) {
|
||||
this.redis.condition.subscriber = new SubscriptionSet_1.default();
|
||||
}
|
||||
const channel = reply[1].toString();
|
||||
this.redis.condition.subscriber.add(replyType, channel);
|
||||
const item = this.shiftCommand(reply);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (!fillSubCommand(item.command, reply[2])) {
|
||||
this.redis.commandQueue.unshift(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "sunsubscribe":
|
||||
case "unsubscribe":
|
||||
case "punsubscribe": {
|
||||
if (this.redis.condition.subscriber) {
|
||||
const channel = reply[1] ? reply[1].toString() : null;
|
||||
if (channel) {
|
||||
this.redis.condition.subscriber.del(replyType, channel);
|
||||
}
|
||||
}
|
||||
const count = reply[2];
|
||||
if (Number(count) === 0) {
|
||||
this.redis.condition.subscriber = false;
|
||||
}
|
||||
if (this.handleUnsolicitedUnsubscribe(replyType)) {
|
||||
return;
|
||||
}
|
||||
const item = this.shiftCommand(reply);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (!fillUnsubCommand(item.command, count)) {
|
||||
this.redis.commandQueue.unshift(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cluster sends unsolicited `sunsubscribe` pushes on slot migration; those
|
||||
// answer no queued command, so shifting the queue head would resolve an
|
||||
// unrelated in-flight command. Detected via a non-matching queue head, and
|
||||
// `sunsubscribe` reuses the ssubscribe-MOVED recovery path.
|
||||
handleUnsolicitedUnsubscribe(replyType) {
|
||||
const head = this.redis.commandQueue.peekFront();
|
||||
// Command names keep the user's casing (e.g. `call("UNSUBSCRIBE")`), while
|
||||
// reply types from the server are lowercase — compare case-insensitively
|
||||
// or an uppercase unsubscribe would be treated as unsolicited and its
|
||||
// reply dropped, desyncing the queue.
|
||||
if (head && head.command.name.toLowerCase() === replyType) {
|
||||
return false;
|
||||
}
|
||||
if (replyType === "sunsubscribe") {
|
||||
this.redis.emit("moved");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
handleSubscriberReply(reply) {
|
||||
if (!this.redis.condition.subscriber) {
|
||||
return false;
|
||||
}
|
||||
const replyType = Array.isArray(reply) ? reply[0].toString() : null;
|
||||
debug('receive reply "%s" in subscriber mode', replyType);
|
||||
switch (replyType) {
|
||||
case "message":
|
||||
if (this.redis.listeners("message").length > 0) {
|
||||
// Check if there're listeners to avoid unnecessary `toString()`.
|
||||
this.redis.emit("message", reply[1].toString(), reply[2] ? reply[2].toString() : "");
|
||||
}
|
||||
this.redis.emit("messageBuffer", reply[1], reply[2]);
|
||||
break;
|
||||
case "pmessage": {
|
||||
const pattern = reply[1].toString();
|
||||
if (this.redis.listeners("pmessage").length > 0) {
|
||||
this.redis.emit("pmessage", pattern, reply[2].toString(), reply[3].toString());
|
||||
}
|
||||
this.redis.emit("pmessageBuffer", pattern, reply[2], reply[3]);
|
||||
break;
|
||||
}
|
||||
case "smessage": {
|
||||
if (this.redis.listeners("smessage").length > 0) {
|
||||
this.redis.emit("smessage", reply[1].toString(), reply[2] ? reply[2].toString() : "");
|
||||
}
|
||||
this.redis.emit("smessageBuffer", reply[1], reply[2]);
|
||||
break;
|
||||
}
|
||||
case "ssubscribe":
|
||||
case "subscribe":
|
||||
case "psubscribe": {
|
||||
const channel = reply[1].toString();
|
||||
this.redis.condition.subscriber.add(replyType, channel);
|
||||
const item = this.shiftCommand(reply);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (!fillSubCommand(item.command, reply[2])) {
|
||||
this.redis.commandQueue.unshift(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "sunsubscribe":
|
||||
case "unsubscribe":
|
||||
case "punsubscribe": {
|
||||
const channel = reply[1] ? reply[1].toString() : null;
|
||||
if (channel) {
|
||||
this.redis.condition.subscriber.del(replyType, channel);
|
||||
}
|
||||
const count = reply[2];
|
||||
if (Number(count) === 0) {
|
||||
this.redis.condition.subscriber = false;
|
||||
}
|
||||
if (this.handleUnsolicitedUnsubscribe(replyType)) {
|
||||
break;
|
||||
}
|
||||
const item = this.shiftCommand(reply);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (!fillUnsubCommand(item.command, count)) {
|
||||
this.redis.commandQueue.unshift(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const item = this.shiftCommand(reply);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
item.command.resolve(reply);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
handleMonitorReply(reply) {
|
||||
if (this.redis.status !== "monitoring") {
|
||||
return false;
|
||||
}
|
||||
const replyStr = reply.toString();
|
||||
if (replyStr === "OK") {
|
||||
// Valid commands in the monitoring mode are AUTH and MONITOR,
|
||||
// both of which always reply with 'OK'.
|
||||
// So if we got an 'OK', we can make certain that
|
||||
// the reply is made to AUTH & MONITOR.
|
||||
return false;
|
||||
}
|
||||
// Since commands sent in the monitoring mode will trigger an exception,
|
||||
// any replies we received in the monitoring mode should consider to be
|
||||
// realtime monitor data instead of result of commands.
|
||||
const len = replyStr.indexOf(" ");
|
||||
const timestamp = replyStr.slice(0, len);
|
||||
const argIndex = replyStr.indexOf('"');
|
||||
const args = replyStr
|
||||
.slice(argIndex + 1, -1)
|
||||
.split('" "')
|
||||
.map((elem) => elem.replace(/\\"/g, '"'));
|
||||
const dbAndSource = replyStr.slice(len + 2, argIndex - 2).split(" ");
|
||||
this.redis.emit("monitor", timestamp, args, dbAndSource[1], dbAndSource[0]);
|
||||
return true;
|
||||
}
|
||||
shiftCommand(reply) {
|
||||
const item = this.redis.commandQueue.shift();
|
||||
if (!item) {
|
||||
const message = "Command queue state error. If you can reproduce this, please report it.";
|
||||
const error = new Error(message +
|
||||
(reply instanceof Error
|
||||
? ` Last error: ${reply.message}`
|
||||
: ` Last reply: ${reply.toString()}`));
|
||||
this.redis.emit("error", error);
|
||||
return null;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
exports.default = DataHandler;
|
||||
// All string RESP types (simple, blob, verbatim) decode to Buffer rather than
|
||||
// utf8 strings. This keeps the parser encoding-agnostic: ioredis decides
|
||||
// utf8-vs-Buffer per command afterwards via `replyEncoding`. Decoding to utf8
|
||||
// here would be irreversible and corrupt binary-safe values (e.g. DUMP output,
|
||||
// serialized blobs), whereas bytes -> string stays lossless and deferred. This
|
||||
// mirrors the old `redis-parser` running with `returnBuffers: true`.
|
||||
// RESP2-compatible shapes: RESP3-only container and numeric types are
|
||||
// flattened so replies are identical across both protocols. Strings stay
|
||||
// buffers; utf8 conversion is decided per command by `replyEncoding`.
|
||||
const legacyTypeMapping = {
|
||||
[decoder_1.RESP_TYPES.SIMPLE_STRING]: Buffer,
|
||||
[decoder_1.RESP_TYPES.BLOB_STRING]: Buffer,
|
||||
[decoder_1.RESP_TYPES.VERBATIM_STRING]: Buffer,
|
||||
[decoder_1.RESP_TYPES.BIG_NUMBER]: String,
|
||||
// RESP2 represented doubles as bulk strings and booleans as `1`/`0`
|
||||
// integers. Decode doubles to Buffer (like blob strings, so `replyEncoding`
|
||||
// still chooses string-vs-buffer per command) and booleans to numbers, so
|
||||
// RESP3 replies match the classic RESP2 shape.
|
||||
[decoder_1.RESP_TYPES.DOUBLE]: Buffer,
|
||||
[decoder_1.RESP_TYPES.BOOLEAN]: Number,
|
||||
[decoder_1.RESP_TYPES.MAP]: Array,
|
||||
[decoder_1.RESP_TYPES.SET]: Array,
|
||||
};
|
||||
// Leaving MAP and DOUBLE unmapped makes the decoder produce plain objects
|
||||
// (with string keys) and numbers respectively.
|
||||
const resp3TypeMapping = {
|
||||
[decoder_1.RESP_TYPES.SIMPLE_STRING]: Buffer,
|
||||
[decoder_1.RESP_TYPES.BLOB_STRING]: Buffer,
|
||||
[decoder_1.RESP_TYPES.VERBATIM_STRING]: Buffer,
|
||||
[decoder_1.RESP_TYPES.BIG_NUMBER]: String,
|
||||
[decoder_1.RESP_TYPES.SET]: Array,
|
||||
};
|
||||
function getParserTypeMapping(parserOptions) {
|
||||
const base = parserOptions.replyMapping === "resp3"
|
||||
? resp3TypeMapping
|
||||
: legacyTypeMapping;
|
||||
// `stringNumbers` means "all numerics as strings", so it wins over the
|
||||
// preset for DOUBLE as well.
|
||||
return parserOptions.stringNumbers
|
||||
? { ...base, [decoder_1.RESP_TYPES.NUMBER]: String, [decoder_1.RESP_TYPES.DOUBLE]: String }
|
||||
: base;
|
||||
}
|
||||
const remainingRepliesMap = new WeakMap();
|
||||
function fillSubCommand(command, count) {
|
||||
let remainingReplies = remainingRepliesMap.has(command)
|
||||
? remainingRepliesMap.get(command)
|
||||
: command.args.length;
|
||||
remainingReplies -= 1;
|
||||
if (remainingReplies <= 0) {
|
||||
command.resolve(count);
|
||||
remainingRepliesMap.delete(command);
|
||||
return true;
|
||||
}
|
||||
remainingRepliesMap.set(command, remainingReplies);
|
||||
return false;
|
||||
}
|
||||
function fillUnsubCommand(command, count) {
|
||||
let remainingReplies = remainingRepliesMap.has(command)
|
||||
? remainingRepliesMap.get(command)
|
||||
: command.args.length;
|
||||
if (remainingReplies === 0) {
|
||||
if (Number(count) === 0) {
|
||||
remainingRepliesMap.delete(command);
|
||||
command.resolve(count);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
remainingReplies -= 1;
|
||||
if (remainingReplies <= 0) {
|
||||
command.resolve(count);
|
||||
return true;
|
||||
}
|
||||
remainingRepliesMap.set(command, remainingReplies);
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user