// @ts-nocheck import { VerbatimString } from './verbatim-string'; import { SimpleError, BlobError, ErrorReply } from '../errors'; import { TypeMapping } from './types'; // https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md export const RESP_TYPES = { NULL: 95, // _ BOOLEAN: 35, // # NUMBER: 58, // : BIG_NUMBER: 40, // ( DOUBLE: 44, // , SIMPLE_STRING: 43, // + BLOB_STRING: 36, // $ VERBATIM_STRING: 61, // = SIMPLE_ERROR: 45, // - BLOB_ERROR: 33, // ! ARRAY: 42, // * SET: 126, // ~ MAP: 37, // % PUSH: 62 // > } as const; const ASCII = { '\r': 13, 't': 116, '+': 43, '-': 45, '0': 48, '.': 46, 'i': 105, 'n': 110, 'E': 69, 'e': 101 } as const; export const PUSH_TYPE_MAPPING = { [RESP_TYPES.BLOB_STRING]: Buffer }; // this was written with performance in mind, so it's not very readable... sorry :( interface DecoderOptions { onReply(reply: any): unknown; onErrorReply(err: ErrorReply): unknown; onPush(push: Array): unknown; getTypeMapping(): TypeMapping; } export class Decoder { onReply; onErrorReply; onPush; getTypeMapping; #cursor = 0; #next; constructor(config: DecoderOptions) { this.onReply = config.onReply; this.onErrorReply = config.onErrorReply; this.onPush = config.onPush; this.getTypeMapping = config.getTypeMapping; } reset() { this.#cursor = 0; this.#next = undefined; } write(chunk) { if (this.#cursor >= chunk.length) { this.#cursor -= chunk.length; return; } if (this.#next) { if (this.#next(chunk) || this.#cursor >= chunk.length) { this.#cursor -= chunk.length; return; } } do { const type = chunk[this.#cursor]; if (++this.#cursor === chunk.length) { this.#next = this.#continueDecodeTypeValue.bind(this, type); break; } if (this.#decodeTypeValue(type, chunk)) { break; } } while (this.#cursor < chunk.length); this.#cursor -= chunk.length; } #continueDecodeTypeValue(type, chunk) { this.#next = undefined; return this.#decodeTypeValue(type, chunk); } #decodeTypeValue(type, chunk) { switch (type) { case RESP_TYPES.NULL: this.onReply(this.#decodeNull()); return false; case RESP_TYPES.BOOLEAN: return this.#handleDecodedValue( this.onReply, this.#decodeBoolean(chunk) ); case RESP_TYPES.NUMBER: return this.#handleDecodedValue( this.onReply, this.#decodeNumber( this.getTypeMapping()[RESP_TYPES.NUMBER], chunk ) ); case RESP_TYPES.BIG_NUMBER: return this.#handleDecodedValue( this.onReply, this.#decodeBigNumber( this.getTypeMapping()[RESP_TYPES.BIG_NUMBER], chunk ) ); case RESP_TYPES.DOUBLE: return this.#handleDecodedValue( this.onReply, this.#decodeDouble( this.getTypeMapping()[RESP_TYPES.DOUBLE], chunk ) ); case RESP_TYPES.SIMPLE_STRING: return this.#handleDecodedValue( this.onReply, this.#decodeSimpleString( this.getTypeMapping()[RESP_TYPES.SIMPLE_STRING], chunk ) ); case RESP_TYPES.BLOB_STRING: return this.#handleDecodedValue( this.onReply, this.#decodeBlobString( this.getTypeMapping()[RESP_TYPES.BLOB_STRING], chunk ) ); case RESP_TYPES.VERBATIM_STRING: return this.#handleDecodedValue( this.onReply, this.#decodeVerbatimString( this.getTypeMapping()[RESP_TYPES.VERBATIM_STRING], chunk ) ); case RESP_TYPES.SIMPLE_ERROR: return this.#handleDecodedValue( this.onErrorReply, this.#decodeSimpleError(chunk) ); case RESP_TYPES.BLOB_ERROR: return this.#handleDecodedValue( this.onErrorReply, this.#decodeBlobError(chunk) ); case RESP_TYPES.ARRAY: return this.#handleDecodedValue( this.onReply, this.#decodeArray(this.getTypeMapping(), chunk) ); case RESP_TYPES.SET: return this.#handleDecodedValue( this.onReply, this.#decodeSet(this.getTypeMapping(), chunk) ); case RESP_TYPES.MAP: return this.#handleDecodedValue( this.onReply, this.#decodeMap(this.getTypeMapping(), chunk) ); case RESP_TYPES.PUSH: return this.#handleDecodedValue( this.onPush, this.#decodeArray(PUSH_TYPE_MAPPING, chunk) ); default: throw new Error(`Unknown RESP type ${type} "${String.fromCharCode(type)}"`); } } #handleDecodedValue(cb, value) { if (typeof value === 'function') { this.#next = this.#continueDecodeValue.bind(this, cb, value); return true; } cb(value); return false; } #continueDecodeValue(cb, next, chunk) { this.#next = undefined; return this.#handleDecodedValue(cb, next(chunk)); } #decodeNull() { this.#cursor += 2; // skip \r\n return null; } #decodeBoolean(chunk) { const boolean = chunk[this.#cursor] === ASCII.t; this.#cursor += 3; // skip {t | f}\r\n return boolean; } #decodeNumber(type, chunk) { if (type === String) { return this.#decodeSimpleString(String, chunk); } switch (chunk[this.#cursor]) { case ASCII['+']: return this.#maybeDecodeNumberValue(false, chunk); case ASCII['-']: return this.#maybeDecodeNumberValue(true, chunk); default: return this.#decodeNumberValue( false, this.#decodeUnsingedNumber.bind(this, 0), chunk ); } } #maybeDecodeNumberValue(isNegative, chunk) { const cb = this.#decodeUnsingedNumber.bind(this, 0); return ++this.#cursor === chunk.length ? this.#decodeNumberValue.bind(this, isNegative, cb) : this.#decodeNumberValue(isNegative, cb, chunk); } #decodeNumberValue(isNegative, numberCb, chunk) { const number = numberCb(chunk); return typeof number === 'function' ? this.#decodeNumberValue.bind(this, isNegative, number) : isNegative ? -number : number; } #decodeUnsingedNumber(number, chunk) { let cursor = this.#cursor; do { const byte = chunk[cursor]; if (byte === ASCII['\r']) { this.#cursor = cursor + 2; // skip \r\n return number; } number = number * 10 + byte - ASCII['0']; } while (++cursor < chunk.length); this.#cursor = cursor; return this.#decodeUnsingedNumber.bind(this, number); } #decodeBigNumber(type, chunk) { if (type === String) { return this.#decodeSimpleString(String, chunk); } switch (chunk[this.#cursor]) { case ASCII['+']: return this.#maybeDecodeBigNumberValue(false, chunk); case ASCII['-']: return this.#maybeDecodeBigNumberValue(true, chunk); default: return this.#decodeBigNumberValue( false, this.#decodeUnsingedBigNumber.bind(this, 0n), chunk ); } } #maybeDecodeBigNumberValue(isNegative, chunk) { const cb = this.#decodeUnsingedBigNumber.bind(this, 0n); return ++this.#cursor === chunk.length ? this.#decodeBigNumberValue.bind(this, isNegative, cb) : this.#decodeBigNumberValue(isNegative, cb, chunk); } #decodeBigNumberValue(isNegative, bigNumberCb, chunk) { const bigNumber = bigNumberCb(chunk); return typeof bigNumber === 'function' ? this.#decodeBigNumberValue.bind(this, isNegative, bigNumber) : isNegative ? -bigNumber : bigNumber; } #decodeUnsingedBigNumber(bigNumber, chunk) { let cursor = this.#cursor; do { const byte = chunk[cursor]; if (byte === ASCII['\r']) { this.#cursor = cursor + 2; // skip \r\n return bigNumber; } bigNumber = bigNumber * 10n + BigInt(byte - ASCII['0']); } while (++cursor < chunk.length); this.#cursor = cursor; return this.#decodeUnsingedBigNumber.bind(this, bigNumber); } #decodeDouble(type, chunk) { if (type === String) { return this.#decodeSimpleString(String, chunk); } switch (chunk[this.#cursor]) { case ASCII.n: this.#cursor += 5; // skip nan\r\n return NaN; case ASCII['+']: return this.#maybeDecodeDoubleInteger(false, chunk); case ASCII['-']: return this.#maybeDecodeDoubleInteger(true, chunk); default: return this.#decodeDoubleInteger(false, 0, chunk); } } #maybeDecodeDoubleInteger(isNegative, chunk) { return ++this.#cursor === chunk.length ? this.#decodeDoubleInteger.bind(this, isNegative, 0) : this.#decodeDoubleInteger(isNegative, 0, chunk); } #decodeDoubleInteger(isNegative, integer, chunk) { if (chunk[this.#cursor] === ASCII.i) { this.#cursor += 5; // skip inf\r\n return isNegative ? -Infinity : Infinity; } return this.#continueDecodeDoubleInteger(isNegative, integer, chunk); } #continueDecodeDoubleInteger(isNegative, integer, chunk) { let cursor = this.#cursor; do { const byte = chunk[cursor]; switch (byte) { case ASCII['.']: this.#cursor = cursor + 1; // skip . return this.#cursor < chunk.length ? this.#decodeDoubleDecimal(isNegative, 0, integer, chunk) : this.#decodeDoubleDecimal.bind(this, isNegative, 0, integer); case ASCII.E: case ASCII.e: this.#cursor = cursor + 1; // skip E/e const i = isNegative ? -integer : integer; return this.#cursor < chunk.length ? this.#decodeDoubleExponent(i, chunk) : this.#decodeDoubleExponent.bind(this, i); case ASCII['\r']: this.#cursor = cursor + 2; // skip \r\n return isNegative ? -integer : integer; default: integer = integer * 10 + byte - ASCII['0']; } } while (++cursor < chunk.length); this.#cursor = cursor; return this.#continueDecodeDoubleInteger.bind(this, isNegative, integer); } // Precalculated multipliers for decimal points to improve performance // "... about 15 to 17 decimal places ..." // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#:~:text=about%2015%20to%2017%20decimal%20places static #DOUBLE_DECIMAL_MULTIPLIERS = [ 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15, 1e-16, 1e-17 ]; #decodeDoubleDecimal(isNegative, decimalIndex, double, chunk) { let cursor = this.#cursor; do { const byte = chunk[cursor]; switch (byte) { case ASCII.E: case ASCII.e: this.#cursor = cursor + 1; // skip E/e const d = isNegative ? -double : double; return this.#cursor === chunk.length ? this.#decodeDoubleExponent.bind(this, d) : this.#decodeDoubleExponent(d, chunk); case ASCII['\r']: this.#cursor = cursor + 2; // skip \r\n return isNegative ? -double : double; } if (decimalIndex < Decoder.#DOUBLE_DECIMAL_MULTIPLIERS.length) { double += (byte - ASCII['0']) * Decoder.#DOUBLE_DECIMAL_MULTIPLIERS[decimalIndex++]; } } while (++cursor < chunk.length); this.#cursor = cursor; return this.#decodeDoubleDecimal.bind(this, isNegative, decimalIndex, double); } #decodeDoubleExponent(double, chunk) { switch (chunk[this.#cursor]) { case ASCII['+']: return ++this.#cursor === chunk.length ? this.#continueDecodeDoubleExponent.bind(this, false, double, 0) : this.#continueDecodeDoubleExponent(false, double, 0, chunk); case ASCII['-']: return ++this.#cursor === chunk.length ? this.#continueDecodeDoubleExponent.bind(this, true, double, 0) : this.#continueDecodeDoubleExponent(true, double, 0, chunk); } return this.#continueDecodeDoubleExponent(false, double, 0, chunk); } #continueDecodeDoubleExponent(isNegative, double, exponent, chunk) { let cursor = this.#cursor; do { const byte = chunk[cursor]; if (byte === ASCII['\r']) { this.#cursor = cursor + 2; // skip \r\n return double * 10 ** (isNegative ? -exponent : exponent); } exponent = exponent * 10 + byte - ASCII['0']; } while (++cursor < chunk.length); this.#cursor = cursor; return this.#continueDecodeDoubleExponent.bind(this, isNegative, double, exponent); } #findCRLF(chunk, cursor) { while (chunk[cursor] !== ASCII['\r']) { if (++cursor === chunk.length) { this.#cursor = chunk.length; return -1; } } this.#cursor = cursor + 2; // skip \r\n return cursor; } #decodeSimpleString(type, chunk) { const start = this.#cursor, crlfIndex = this.#findCRLF(chunk, start); if (crlfIndex === -1) { return this.#continueDecodeSimpleString.bind( this, [chunk.subarray(start)], type ); } const slice = chunk.subarray(start, crlfIndex); return type === Buffer ? slice : slice.toString(); } #continueDecodeSimpleString(chunks, type, chunk) { const start = this.#cursor, crlfIndex = this.#findCRLF(chunk, start); if (crlfIndex === -1) { chunks.push(chunk.subarray(start)); return this.#continueDecodeSimpleString.bind(this, chunks, type); } chunks.push(chunk.subarray(start, crlfIndex)); return type === Buffer ? Buffer.concat(chunks) : chunks.join(''); } #decodeBlobString(type, chunk) { // RESP 2 bulk string null // https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md#resp-bulk-strings if (chunk[this.#cursor] === ASCII['-']) { this.#cursor += 4; // skip -1\r\n return null; } const length = this.#decodeUnsingedNumber(0, chunk); if (typeof length === 'function') { return this.#continueDecodeBlobStringLength.bind(this, length, type); } else if (this.#cursor >= chunk.length) { return this.#decodeBlobStringWithLength.bind(this, length, type); } return this.#decodeBlobStringWithLength(length, type, chunk); } #continueDecodeBlobStringLength(lengthCb, type, chunk) { const length = lengthCb(chunk); if (typeof length === 'function') { return this.#continueDecodeBlobStringLength.bind(this, length, type); } else if (this.#cursor >= chunk.length) { return this.#decodeBlobStringWithLength.bind(this, length, type); } return this.#decodeBlobStringWithLength(length, type, chunk); } #decodeStringWithLength(length, skip, type, chunk) { const end = this.#cursor + length; if (end >= chunk.length) { const slice = chunk.subarray(this.#cursor); this.#cursor = chunk.length; return this.#continueDecodeStringWithLength.bind( this, length - slice.length, [slice], skip, type ); } const slice = chunk.subarray(this.#cursor, end); this.#cursor = end + skip; return type === Buffer ? slice : slice.toString(); } #continueDecodeStringWithLength(length, chunks, skip, type, chunk) { const end = this.#cursor + length; if (end >= chunk.length) { const slice = chunk.subarray(this.#cursor); chunks.push(slice); this.#cursor = chunk.length; return this.#continueDecodeStringWithLength.bind( this, length - slice.length, chunks, skip, type ); } chunks.push(chunk.subarray(this.#cursor, end)); this.#cursor = end + skip; return type === Buffer ? Buffer.concat(chunks) : chunks.join(''); } #decodeBlobStringWithLength(length, type, chunk) { return this.#decodeStringWithLength(length, 2, type, chunk); } #decodeVerbatimString(type, chunk) { return this.#continueDecodeVerbatimStringLength( this.#decodeUnsingedNumber.bind(this, 0), type, chunk ); } #continueDecodeVerbatimStringLength(lengthCb, type, chunk) { const length = lengthCb(chunk); return typeof length === 'function' ? this.#continueDecodeVerbatimStringLength.bind(this, length, type) : this.#decodeVerbatimStringWithLength(length, type, chunk); } #decodeVerbatimStringWithLength(length, type, chunk) { const stringLength = length - 4; // skip : if (type === VerbatimString) { return this.#decodeVerbatimStringFormat(stringLength, chunk); } this.#cursor += 4; // skip : return this.#cursor >= chunk.length ? this.#decodeBlobStringWithLength.bind(this, stringLength, type) : this.#decodeBlobStringWithLength(stringLength, type, chunk); } #decodeVerbatimStringFormat(stringLength, chunk) { const formatCb = this.#decodeStringWithLength.bind(this, 3, 1, String); return this.#cursor >= chunk.length ? this.#continueDecodeVerbatimStringFormat.bind(this, stringLength, formatCb) : this.#continueDecodeVerbatimStringFormat(stringLength, formatCb, chunk); } #continueDecodeVerbatimStringFormat(stringLength, formatCb, chunk) { const format = formatCb(chunk); return typeof format === 'function' ? this.#continueDecodeVerbatimStringFormat.bind(this, stringLength, format) : this.#decodeVerbatimStringWithFormat(stringLength, format, chunk); } #decodeVerbatimStringWithFormat(stringLength, format, chunk) { return this.#continueDecodeVerbatimStringWithFormat( format, this.#decodeBlobStringWithLength.bind(this, stringLength, String), chunk ); } #continueDecodeVerbatimStringWithFormat(format, stringCb, chunk) { const string = stringCb(chunk); return typeof string === 'function' ? this.#continueDecodeVerbatimStringWithFormat.bind(this, format, string) : new VerbatimString(format, string); } #decodeSimpleError(chunk) { const string = this.#decodeSimpleString(String, chunk); return typeof string === 'function' ? this.#continueDecodeSimpleError.bind(this, string) : new SimpleError(string); } #continueDecodeSimpleError(stringCb, chunk) { const string = stringCb(chunk); return typeof string === 'function' ? this.#continueDecodeSimpleError.bind(this, string) : new SimpleError(string); } #decodeBlobError(chunk) { const string = this.#decodeBlobString(String, chunk); return typeof string === 'function' ? this.#continueDecodeBlobError.bind(this, string) : new BlobError(string); } #continueDecodeBlobError(stringCb, chunk) { const string = stringCb(chunk); return typeof string === 'function' ? this.#continueDecodeBlobError.bind(this, string) : new BlobError(string); } #decodeNestedType(typeMapping, chunk) { const type = chunk[this.#cursor]; return ++this.#cursor === chunk.length ? this.#decodeNestedTypeValue.bind(this, type, typeMapping) : this.#decodeNestedTypeValue(type, typeMapping, chunk); } #decodeNestedTypeValue(type, typeMapping, chunk) { switch (type) { case RESP_TYPES.NULL: return this.#decodeNull(); case RESP_TYPES.BOOLEAN: return this.#decodeBoolean(chunk); case RESP_TYPES.NUMBER: return this.#decodeNumber(typeMapping[RESP_TYPES.NUMBER], chunk); case RESP_TYPES.BIG_NUMBER: return this.#decodeBigNumber(typeMapping[RESP_TYPES.BIG_NUMBER], chunk); case RESP_TYPES.DOUBLE: return this.#decodeDouble(typeMapping[RESP_TYPES.DOUBLE], chunk); case RESP_TYPES.SIMPLE_STRING: return this.#decodeSimpleString(typeMapping[RESP_TYPES.SIMPLE_STRING], chunk); case RESP_TYPES.BLOB_STRING: return this.#decodeBlobString(typeMapping[RESP_TYPES.BLOB_STRING], chunk); case RESP_TYPES.VERBATIM_STRING: return this.#decodeVerbatimString(typeMapping[RESP_TYPES.VERBATIM_STRING], chunk); case RESP_TYPES.SIMPLE_ERROR: return this.#decodeSimpleError(chunk); case RESP_TYPES.BLOB_ERROR: return this.#decodeBlobError(chunk); case RESP_TYPES.ARRAY: return this.#decodeArray(typeMapping, chunk); case RESP_TYPES.SET: return this.#decodeSet(typeMapping, chunk); case RESP_TYPES.MAP: return this.#decodeMap(typeMapping, chunk); default: throw new Error(`Unknown RESP type ${type} "${String.fromCharCode(type)}"`); } } #decodeArray(typeMapping, chunk) { // RESP 2 null // https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md#resp-arrays if (chunk[this.#cursor] === ASCII['-']) { this.#cursor += 4; // skip -1\r\n return null; } return this.#decodeArrayWithLength( this.#decodeUnsingedNumber(0, chunk), typeMapping, chunk ); } #decodeArrayWithLength(length, typeMapping, chunk) { return typeof length === 'function' ? this.#continueDecodeArrayLength.bind(this, length, typeMapping) : this.#decodeArrayItems( new Array(length), 0, typeMapping, chunk ); } #continueDecodeArrayLength(lengthCb, typeMapping, chunk) { return this.#decodeArrayWithLength( lengthCb(chunk), typeMapping, chunk ); } #decodeArrayItems(array, filled, typeMapping, chunk) { for (let i = filled; i < array.length; i++) { if (this.#cursor >= chunk.length) { return this.#decodeArrayItems.bind( this, array, i, typeMapping ); } const item = this.#decodeNestedType(typeMapping, chunk); if (typeof item === 'function') { return this.#continueDecodeArrayItems.bind( this, array, i, item, typeMapping ); } array[i] = item; } return array; } #continueDecodeArrayItems(array, filled, itemCb, typeMapping, chunk) { const item = itemCb(chunk); if (typeof item === 'function') { return this.#continueDecodeArrayItems.bind( this, array, filled, item, typeMapping ); } array[filled++] = item; return this.#decodeArrayItems(array, filled, typeMapping, chunk); } #decodeSet(typeMapping, chunk) { const length = this.#decodeUnsingedNumber(0, chunk); if (typeof length === 'function') { return this.#continueDecodeSetLength.bind(this, length, typeMapping); } return this.#decodeSetItems( length, typeMapping, chunk ); } #continueDecodeSetLength(lengthCb, typeMapping, chunk) { const length = lengthCb(chunk); return typeof length === 'function' ? this.#continueDecodeSetLength.bind(this, length, typeMapping) : this.#decodeSetItems(length, typeMapping, chunk); } #decodeSetItems(length, typeMapping, chunk) { return typeMapping[RESP_TYPES.SET] === Set ? this.#decodeSetAsSet( new Set(), length, typeMapping, chunk ) : this.#decodeArrayItems( new Array(length), 0, typeMapping, chunk ); } #decodeSetAsSet(set, remaining, typeMapping, chunk) { // using `remaining` instead of `length` & `set.size` to make it work even if the set contains duplicates while (remaining > 0) { if (this.#cursor >= chunk.length) { return this.#decodeSetAsSet.bind( this, set, remaining, typeMapping ); } const item = this.#decodeNestedType(typeMapping, chunk); if (typeof item === 'function') { return this.#continueDecodeSetAsSet.bind( this, set, remaining, item, typeMapping ); } set.add(item); --remaining; } return set; } #continueDecodeSetAsSet(set, remaining, itemCb, typeMapping, chunk) { const item = itemCb(chunk); if (typeof item === 'function') { return this.#continueDecodeSetAsSet.bind( this, set, remaining, item, typeMapping ); } set.add(item); return this.#decodeSetAsSet(set, remaining - 1, typeMapping, chunk); } #decodeMap(typeMapping, chunk) { const length = this.#decodeUnsingedNumber(0, chunk); if (typeof length === 'function') { return this.#continueDecodeMapLength.bind(this, length, typeMapping); } return this.#decodeMapItems( length, typeMapping, chunk ); } #continueDecodeMapLength(lengthCb, typeMapping, chunk) { const length = lengthCb(chunk); return typeof length === 'function' ? this.#continueDecodeMapLength.bind(this, length, typeMapping) : this.#decodeMapItems(length, typeMapping, chunk); } #decodeMapItems(length, typeMapping, chunk) { switch (typeMapping[RESP_TYPES.MAP]) { case Map: return this.#decodeMapAsMap( new Map(), length, typeMapping, chunk ); case Array: return this.#decodeArrayItems( new Array(length * 2), 0, typeMapping, chunk ); default: return this.#decodeMapAsObject( Object.create(null), length, typeMapping, chunk ); } } #decodeMapAsMap(map, remaining, typeMapping, chunk) { // using `remaining` instead of `length` & `map.size` to make it work even if the map contains duplicate keys while (remaining > 0) { if (this.#cursor >= chunk.length) { return this.#decodeMapAsMap.bind( this, map, remaining, typeMapping ); } const key = this.#decodeMapKey(typeMapping, chunk); if (typeof key === 'function') { return this.#continueDecodeMapKey.bind( this, map, remaining, key, typeMapping ); } if (this.#cursor >= chunk.length) { return this.#continueDecodeMapValue.bind( this, map, remaining, key, this.#decodeNestedType.bind(this, typeMapping), typeMapping ); } const value = this.#decodeNestedType(typeMapping, chunk); if (typeof value === 'function') { return this.#continueDecodeMapValue.bind( this, map, remaining, key, value, typeMapping ); } map.set(key, value); --remaining; } return map; } #decodeMapKey(typeMapping, chunk) { const type = chunk[this.#cursor]; return ++this.#cursor === chunk.length ? this.#decodeMapKeyValue.bind(this, type, typeMapping) : this.#decodeMapKeyValue(type, typeMapping, chunk); } #decodeMapKeyValue(type, typeMapping, chunk) { switch (type) { // decode simple string map key as string (and not as buffer) case RESP_TYPES.SIMPLE_STRING: return this.#decodeSimpleString(String, chunk); // decode blob string map key as string (and not as buffer) case RESP_TYPES.BLOB_STRING: return this.#decodeBlobString(String, chunk); default: return this.#decodeNestedTypeValue(type, typeMapping, chunk); } } #continueDecodeMapKey(map, remaining, keyCb, typeMapping, chunk) { const key = keyCb(chunk); if (typeof key === 'function') { return this.#continueDecodeMapKey.bind( this, map, remaining, key, typeMapping ); } if (this.#cursor >= chunk.length) { return this.#continueDecodeMapValue.bind( this, map, remaining, key, this.#decodeNestedType.bind(this, typeMapping), typeMapping ); } const value = this.#decodeNestedType(typeMapping, chunk); if (typeof value === 'function') { return this.#continueDecodeMapValue.bind( this, map, remaining, key, value, typeMapping ); } map.set(key, value); return this.#decodeMapAsMap(map, remaining - 1, typeMapping, chunk); } #continueDecodeMapValue(map, remaining, key, valueCb, typeMapping, chunk) { const value = valueCb(chunk); if (typeof value === 'function') { return this.#continueDecodeMapValue.bind( this, map, remaining, key, value, typeMapping ); } map.set(key, value); return this.#decodeMapAsMap(map, remaining - 1, typeMapping, chunk); } #decodeMapAsObject(object, remaining, typeMapping, chunk) { while (remaining > 0) { if (this.#cursor >= chunk.length) { return this.#decodeMapAsObject.bind( this, object, remaining, typeMapping ); } const key = this.#decodeMapKey(typeMapping, chunk); if (typeof key === 'function') { return this.#continueDecodeMapAsObjectKey.bind( this, object, remaining, key, typeMapping ); } if (this.#cursor >= chunk.length) { return this.#continueDecodeMapAsObjectValue.bind( this, object, remaining, key, this.#decodeNestedType.bind(this, typeMapping), typeMapping ); } const value = this.#decodeNestedType(typeMapping, chunk); if (typeof value === 'function') { return this.#continueDecodeMapAsObjectValue.bind( this, object, remaining, key, value, typeMapping ); } object[key] = value; --remaining; } return object; } #continueDecodeMapAsObjectKey(object, remaining, keyCb, typeMapping, chunk) { const key = keyCb(chunk); if (typeof key === 'function') { return this.#continueDecodeMapAsObjectKey.bind( this, object, remaining, key, typeMapping ); } if (this.#cursor >= chunk.length) { return this.#continueDecodeMapAsObjectValue.bind( this, object, remaining, key, this.#decodeNestedType.bind(this, typeMapping), typeMapping ); } const value = this.#decodeNestedType(typeMapping, chunk); if (typeof value === 'function') { return this.#continueDecodeMapAsObjectValue.bind( this, object, remaining, key, value, typeMapping ); } object[key] = value; return this.#decodeMapAsObject(object, remaining - 1, typeMapping, chunk); } #continueDecodeMapAsObjectValue(object, remaining, key, valueCb, typeMapping, chunk) { const value = valueCb(chunk); if (typeof value === 'function') { return this.#continueDecodeMapAsObjectValue.bind( this, object, remaining, key, value, typeMapping ); } object[key] = value; return this.#decodeMapAsObject(object, remaining - 1, typeMapping, chunk); } }