From 4b3913d74bcbdfd2d0a6ad3c07d9e5036f4d3831 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 10:44:14 -0700 Subject: [PATCH 01/11] Replace not human-readable values with readable phrase. Signed-off-by: Argus Li --- apps/server/src/keys-browser.ts | 18 +++++++------- apps/server/src/utils.ts | 42 ++++++++++++++++++++++++++++++++- common/src/constants.ts | 4 ++++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index b36d109f..84e58354 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -6,12 +6,14 @@ import { RouteOption, ConnectionError, TimeoutError, - ClosingError + ClosingError, + GlideReturnType } from "@valkey/valkey-glide" import pLimit from "p-limit" import { VALKEY, VALKEY_CLIENT } from "../../../common/src/constants.ts" import { buildScanCommandArgs } from "./valkey-client-commands.ts" import { formatBytes } from "../../../common/src/bytes-conversion.ts" +import { getHumanReadableElement } from "./utils.ts" interface EnrichedKeyInfo { name: string; @@ -29,8 +31,7 @@ async function getScanKeyInfo( commands: { sizeCmd: string; elementsCmd: string[] }, ): Promise { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const results = new Set() + const results = new Set() const isHash = keyInfo.type.toLowerCase() === "hash" let cursor = "0" @@ -40,17 +41,16 @@ async function getScanKeyInfo( client.customCommand([commands.sizeCmd, keyInfo.name]), (async () => { do { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [newCursor, elements] = await client.customCommand([...commands.elementsCmd, cursor]) as [string, any[]] + const [newCursor, elements] = await client.customCommand([...commands.elementsCmd, cursor]) as [string, GlideReturnType[]] if (isHash) { // Hash key types require constructing an object from a flat array. // i.e. converting [key1, value1...] to [{key: key1, value}] for (let i = 0; i < elements.length; i += 2){ - results.add({ key: elements[i], value: elements[i + 1] }) + results.add({ key: getHumanReadableElement(elements[i]), value: getHumanReadableElement(elements[i + 1]) }) } } else { - elements.forEach((element) => results.add(element)) + elements.forEach((element) => results.add(getHumanReadableElement(element))) } cursor = newCursor } while (cursor !== "0") @@ -86,13 +86,13 @@ async function getFullKeyInfo( return { ...keyInfo, collectionSize: results[1] as number, - elements: results[0], + elements: getHumanReadableElement(results[0]), } } else { // in case of string with no collectionSize return { ...keyInfo, - elements: results[0], + elements: getHumanReadableElement(results[0]), } } } catch (err) { diff --git a/apps/server/src/utils.ts b/apps/server/src/utils.ts index 3813823d..3444b09c 100644 --- a/apps/server/src/utils.ts +++ b/apps/server/src/utils.ts @@ -1,7 +1,8 @@ -import { ClusterResponse, GlideClient, GlideClusterClient } from "@valkey/valkey-glide" +import { ClusterResponse, GlideClient, GlideClusterClient, GlideReturnType } from "@valkey/valkey-glide" import * as R from "ramda" import { lookup, reverse } from "node:dns/promises" import { sanitizeUrl } from "../../../common/src/url-utils" +import { VALKEY_CLIENT } from "../../../common/src/constants" export const dns = { lookup, @@ -141,3 +142,42 @@ export async function isLastConnectedClusterNode( const currentClusterId = connection?.clusterId return clusterNodesMap.get(currentClusterId!)?.length === 1 } + +const isReadableString = (str: string): boolean => { + return !str.includes('\uFFFD'); // Unicode replacement character when decoding fails +} + +const getPrintableRatio = (str: string): number => { + if (str.length === 0) return 1; + + const printableCount = [...str].filter( + (char) => { + const code = char.charCodeAt(0) + + return ( + (code >= 32 && code <= 126) // letters, numbers, punctuations + || code === 9 // Tab + || code === 10 // Line Feed + || code === 13 // Carriage return + ) + } + ).length + + return printableCount / str.length; +} + +export const isHumanReadable = (data: GlideReturnType): boolean => { + if (typeof data !== 'string') { + return false; + } + + return isReadableString(data) + && getPrintableRatio(data) >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO; +} + +export const getHumanReadableElement = (element: GlideReturnType): string => { + if (typeof element !== "string" || !isHumanReadable(element)){ + return VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE; + } + return element as string; +} diff --git a/common/src/constants.ts b/common/src/constants.ts index ac274931..fb9baff0 100644 --- a/common/src/constants.ts +++ b/common/src/constants.ts @@ -131,6 +131,10 @@ export const VALKEY_CLIENT = { defaultCount: 50, } , KEY_VALUE_SIZE_LIMIT: 2048, // 2KiB + HUMAN_READABLE: { + ACCEPTABLE_PRINTABLE_RATIO: 0.90, + NOT_READABLE_MESSAGE: "Not human readable", + } } export const COMMANDLOG_TYPE = { SLOW: "slow", From e32d49c564c7eb7f1e63f3ce14139996f5943a40 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 12:06:09 -0700 Subject: [PATCH 02/11] Fix issue with accepting arrays or nested arrays. Signed-off-by: Argus Li --- apps/server/src/keys-browser.ts | 9 ++++++--- apps/server/src/utils.ts | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index 84e58354..40141839 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -13,7 +13,7 @@ import pLimit from "p-limit" import { VALKEY, VALKEY_CLIENT } from "../../../common/src/constants.ts" import { buildScanCommandArgs } from "./valkey-client-commands.ts" import { formatBytes } from "../../../common/src/bytes-conversion.ts" -import { getHumanReadableElement } from "./utils.ts" +import { getHumanReadableElement, getHumanReadableString } from "./utils.ts" interface EnrichedKeyInfo { name: string; @@ -47,10 +47,13 @@ async function getScanKeyInfo( // Hash key types require constructing an object from a flat array. // i.e. converting [key1, value1...] to [{key: key1, value}] for (let i = 0; i < elements.length; i += 2){ - results.add({ key: getHumanReadableElement(elements[i]), value: getHumanReadableElement(elements[i + 1]) }) + results.add({ + key: getHumanReadableString(elements[i] as string), + value: getHumanReadableString(elements[i + 1] as string) + }) } } else { - elements.forEach((element) => results.add(getHumanReadableElement(element))) + elements.forEach((element) => results.add(getHumanReadableString(element as string))) } cursor = newCursor } while (cursor !== "0") diff --git a/apps/server/src/utils.ts b/apps/server/src/utils.ts index 3444b09c..630d3427 100644 --- a/apps/server/src/utils.ts +++ b/apps/server/src/utils.ts @@ -175,9 +175,21 @@ export const isHumanReadable = (data: GlideReturnType): boolean => { && getPrintableRatio(data) >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO; } -export const getHumanReadableElement = (element: GlideReturnType): string => { - if (typeof element !== "string" || !isHumanReadable(element)){ - return VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE; +export const getHumanReadableString = (str: string): string => { + return !isHumanReadable(str) + ? VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE + : str; +} + +export type HumanReadableResult = string | HumanReadableResult[]; + +export const getHumanReadableElement = (element: GlideReturnType): HumanReadableResult => { + if (Array.isArray(element)) { + return element.map((item) => getHumanReadableElement(item)); + } else if (typeof element === "string") { + return getHumanReadableString(element); } - return element as string; + + // For non-string, non-array types + return VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE; } From c81b19b0fb8b7a8c3da76f9a40ecfa530a59667a Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 12:06:14 -0700 Subject: [PATCH 03/11] Add tests Signed-off-by: Argus Li --- .../server/src/__tests__/keys-browser.test.ts | 398 +++++++++++++++++- apps/server/src/__tests__/utils.test.ts | 296 ++++++++++++- 2 files changed, 692 insertions(+), 2 deletions(-) diff --git a/apps/server/src/__tests__/keys-browser.test.ts b/apps/server/src/__tests__/keys-browser.test.ts index 03b237dc..28f5bc76 100644 --- a/apps/server/src/__tests__/keys-browser.test.ts +++ b/apps/server/src/__tests__/keys-browser.test.ts @@ -58,6 +58,41 @@ describe("getKeyInfo", () => { assert.strictEqual(result.size, 0) }) + + describe("string keys with non-readable data", () => { + it("should return 'Not human readable' for string with binary value", async () => { + const binaryValue = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "string", + TTL: -1, + MEMORY: 100, + GET: binaryValue, + }) + + const result = await getKeyInfo(mockClient as any, "mykey") + + assert.strictEqual(result.name, "mykey") + assert.strictEqual(result.type, "string") + assert.strictEqual(result.elements, "Not human readable") + }) + + it("should return original value for string at 90% printable threshold", async () => { + // 9 printable characters out of 10 = exactly 90% + const threshold90 = "abcdefghi\x00" + const mockClient = createMockClient({ + TYPE: "string", + TTL: -1, + MEMORY: 100, + GET: threshold90, + }) + + const result = await getKeyInfo(mockClient as any, "mykey") + + assert.strictEqual(result.name, "mykey") + assert.strictEqual(result.type, "string") + assert.strictEqual(result.elements, threshold90) + }) + }) }) describe("hash keys", () => { @@ -82,10 +117,110 @@ describe("getKeyInfo", () => { { key: "field2", value: "value2" }, ]) }) + + describe("hash keys with non-readable data", () => { + it("should return 'Not human readable' for hash with binary values", async () => { + const binaryValue = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "hash", + TTL: -1, + MEMORY: 200, + HLEN: 2, + HSCAN: ["0", ["field1", binaryValue, "field2", "abc\x00\x01\x02\x03\x04\x05\x06\x07\x08"]], + }) + + const result = await getKeyInfo(mockClient as any, "myhash") + + assert.strictEqual(result.name, "myhash") + assert.strictEqual(result.type, "hash") + assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) + const elements = result.elements as Array<{ key: string; value: string }> + assert.ok(elements.some((e) => e.value === "Not human readable")) + }) + + it("should filter mix of readable and non-readable hash values appropriately", async () => { + const binaryValue = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "hash", + TTL: -1, + MEMORY: 200, + HLEN: 3, + HSCAN: ["0", ["field1", "readable", "field2", binaryValue, "field3", "alsoreadable"]], + }) + + const result = await getKeyInfo(mockClient as any, "myhash") + + assert.strictEqual(result.collectionSize, 3) + const elements = result.elements as Array<{ key: string; value: string }> + assert.ok(elements.some((e) => e.key === "field1" && e.value === "readable")) + assert.ok(elements.some((e) => e.key === "field2" && e.value === "Not human readable")) + assert.ok(elements.some((e) => e.key === "field3" && e.value === "alsoreadable")) + }) + + it("should return 'Not human readable' for hash with non-readable keys", async () => { + const binaryKey = "key\uFFFDtest" + const mockClient = createMockClient({ + TYPE: "hash", + TTL: -1, + MEMORY: 200, + HLEN: 2, + HSCAN: ["0", [binaryKey, "value1", "normalkey", "value2"]], + }) + + const result = await getKeyInfo(mockClient as any, "myhash") + + assert.strictEqual(result.collectionSize, 2) + const elements = result.elements as Array<{ key: string; value: string }> + assert.ok(elements.some((e) => e.key === "Not human readable")) + assert.ok(elements.some((e) => e.key === "normalkey" && e.value === "value2")) + }) + }) + }) + + describe("set keys with non-readable data", () => { + it("should return 'Not human readable' for set with binary data elements", async () => { + const binaryData = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "set", + TTL: -1, + MEMORY: 150, + SCARD: 2, + SSCAN: ["0", [binaryData, "abc\x00\x01\x02\x03\x04\x05\x06\x07\x08"]], + }) + + const result = await getKeyInfo(mockClient as any, "myset") + + assert.strictEqual(result.name, "myset") + assert.strictEqual(result.type, "set") + assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) + const elements = result.elements as string[] + assert.ok(elements.includes("Not human readable")) + }) + + it("should filter mix of readable and non-readable set elements appropriately", async () => { + const binaryData = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "set", + TTL: -1, + MEMORY: 150, + SCARD: 3, + SSCAN: ["0", ["readable1", binaryData, "readable2"]], + }) + + const result = await getKeyInfo(mockClient as any, "myset") + + assert.strictEqual(result.collectionSize, 3) + const elements = result.elements as string[] + assert.ok(elements.includes("readable1")) + assert.ok(elements.includes("Not human readable")) + assert.ok(elements.includes("readable2")) + }) }) describe("list keys", () => { - it("should get list key info", async () => { + it("should get list key info with readable elements", async () => { const mockClient = createMockClient({ TYPE: "list", TTL: 0, @@ -99,8 +234,177 @@ describe("getKeyInfo", () => { assert.strictEqual(result.name, "mylist") assert.strictEqual(result.type, "list") assert.strictEqual(result.collectionSize, 2) + // getFullKeyInfo calls getHumanReadableElement on array result + // which returns an array with each element filtered + assert.ok(Array.isArray(result.elements)) assert.deepStrictEqual(result.elements, ["item1", "item2"]) }) + + it("should filter binary data in list elements", async () => { + const binaryData = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "list", + TTL: -1, + MEMORY: 150, + LLEN: 3, + LRANGE: ["readable1", binaryData, "readable2"], + }) + + const result = await getKeyInfo(mockClient as any, "mylist") + + assert.strictEqual(result.name, "mylist") + assert.strictEqual(result.type, "list") + assert.strictEqual(result.collectionSize, 3) + assert.ok(Array.isArray(result.elements)) + const elements = result.elements as string[] + assert.strictEqual(elements[0], "readable1") + assert.strictEqual(elements[1], "Not human readable") + assert.strictEqual(elements[2], "readable2") + }) + }) + + describe("zset keys", () => { + it("should get zset key info with readable members", async () => { + const mockClient = createMockClient({ + TYPE: "zset", + TTL: -1, + MEMORY: 200, + ZCARD: 2, + ZRANGE: ["member1", "1.5", "member2", "2.5"], + }) + + const result = await getKeyInfo(mockClient as any, "myzset") + + assert.strictEqual(result.name, "myzset") + assert.strictEqual(result.type, "zset") + assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) + // ZRANGE with WITHSCORES returns [member1, score1, member2, score2, ...] + assert.deepStrictEqual(result.elements, ["member1", "1.5", "member2", "2.5"]) + }) + + it("should filter binary data in zset members", async () => { + const binaryMember = "member\uFFFDtest" + const mockClient = createMockClient({ + TYPE: "zset", + TTL: -1, + MEMORY: 200, + ZCARD: 3, + ZRANGE: ["readable", "1.0", binaryMember, "2.0", "alsoreadable", "3.0"], + }) + + const result = await getKeyInfo(mockClient as any, "myzset") + + assert.strictEqual(result.name, "myzset") + assert.strictEqual(result.type, "zset") + assert.strictEqual(result.collectionSize, 3) + assert.ok(Array.isArray(result.elements)) + const elements = result.elements as string[] + assert.strictEqual(elements[0], "readable") + assert.strictEqual(elements[1], "1.0") + assert.strictEqual(elements[2], "Not human readable") + assert.strictEqual(elements[3], "2.0") + assert.strictEqual(elements[4], "alsoreadable") + assert.strictEqual(elements[5], "3.0") + }) + }) + + describe("stream keys", () => { + it("should get stream key info with readable entries", async () => { + const mockClient = createMockClient({ + TYPE: "stream", + TTL: -1, + MEMORY: 300, + XLEN: 2, + XRANGE: [ + ["1234567890-0", ["field1", "value1", "field2", "value2"]], + ["1234567891-0", ["field3", "value3"]], + ], + }) + + const result = await getKeyInfo(mockClient as any, "mystream") + + assert.strictEqual(result.name, "mystream") + assert.strictEqual(result.type, "stream") + assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) + // Stream entries are nested arrays: [[id, [field, value, ...]], ...] + const elements = result.elements as string[][] + assert.strictEqual(elements.length, 2) + assert.ok(Array.isArray(elements[0])) + assert.ok(Array.isArray(elements[1])) + }) + + it("should filter binary data in stream field names and values", async () => { + const binaryField = "field\uFFFDtest" + const binaryValue = "value\uFFFDtest" + const mockClient = createMockClient({ + TYPE: "stream", + TTL: -1, + MEMORY: 300, + XLEN: 2, + XRANGE: [ + ["1234567890-0", ["readable_field", "readable_value", binaryField, binaryValue]], + ["1234567891-0", ["field1", binaryValue]], + ], + }) + + const result = await getKeyInfo(mockClient as any, "mystream") + + assert.strictEqual(result.name, "mystream") + assert.strictEqual(result.type, "stream") + assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) + const elements = result.elements as string[][] + + // First entry + assert.strictEqual(elements[0][0], "1234567890-0") + const firstFields = elements[0][1] as string[] + assert.strictEqual(firstFields[0], "readable_field") + assert.strictEqual(firstFields[1], "readable_value") + assert.strictEqual(firstFields[2], "Not human readable") + assert.strictEqual(firstFields[3], "Not human readable") + + // Second entry + assert.strictEqual(elements[1][0], "1234567891-0") + const secondFields = elements[1][1] as string[] + assert.strictEqual(secondFields[0], "field1") + assert.strictEqual(secondFields[1], "Not human readable") + }) + }) + + describe("json keys", () => { + it("should get json key info with readable JSON string", async () => { + const jsonValue = '{"name":"test","value":123}' + const mockClient = createMockClient({ + TYPE: "rejson-rl", + TTL: -1, + MEMORY: 100, + "JSON.GET": jsonValue, + }) + + const result = await getKeyInfo(mockClient as any, "myjson") + + assert.strictEqual(result.name, "myjson") + assert.strictEqual(result.type, "rejson-rl") + assert.strictEqual(result.elements, jsonValue) + }) + + it("should return 'Not human readable' for JSON with binary data", async () => { + const binaryJson = '{"name":"test\uFFFD","value":123}' + const mockClient = createMockClient({ + TYPE: "rejson-rl", + TTL: -1, + MEMORY: 100, + "JSON.GET": binaryJson, + }) + + const result = await getKeyInfo(mockClient as any, "myjson") + + assert.strictEqual(result.name, "myjson") + assert.strictEqual(result.type, "rejson-rl") + assert.strictEqual(result.elements, "Not human readable") + }) }) describe("error handling", () => { @@ -121,6 +425,98 @@ describe("getKeyInfo", () => { }) }) +describe("getScanKeyInfo", () => { + it("should properly filter binary data in hash scan", async () => { + const binaryValue = "test\uFFFDdata" + const mockClient = { + customCommand: mock.fn(async (cmd: string[]) => { + if (cmd[0] === "TYPE") return "hash" + if (cmd[0] === "TTL") return -1 + if (cmd[0] === "MEMORY") return 200 + if (cmd[0] === "HLEN") return 2 + if (cmd[0] === "HSCAN") { + return ["0", ["field1", "readable", "field2", binaryValue]] + } + return null + }), + } + + const result = await getKeyInfo(mockClient as any, "myhash") + + assert.strictEqual(result.collectionSize, 2) + const elements = result.elements as Array<{ key: string; value: string }> + assert.ok(elements.some((e) => e.key === "field1" && e.value === "readable")) + assert.ok(elements.some((e) => e.key === "field2" && e.value === "Not human readable")) + }) + + it("should properly filter binary data in set scan", async () => { + const binaryData = "test\uFFFDdata" + const mockClient = { + customCommand: mock.fn(async (cmd: string[]) => { + if (cmd[0] === "TYPE") return "set" + if (cmd[0] === "TTL") return -1 + if (cmd[0] === "MEMORY") return 150 + if (cmd[0] === "SCARD") return 3 + if (cmd[0] === "SSCAN") { + return ["0", ["readable1", binaryData, "readable2"]] + } + return null + }), + } + + const result = await getKeyInfo(mockClient as any, "myset") + + assert.strictEqual(result.collectionSize, 3) + const elements = result.elements as string[] + assert.ok(elements.includes("readable1")) + assert.ok(elements.includes("Not human readable")) + assert.ok(elements.includes("readable2")) + }) +}) + +describe("getFullKeyInfo", () => { + it("should return 'Not human readable' for string value with binary data", async () => { + const binaryValue = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "string", + TTL: -1, + MEMORY: 100, + GET: binaryValue, + }) + + const result = await getKeyInfo(mockClient as any, "mykey") + + assert.strictEqual(result.name, "mykey") + assert.strictEqual(result.type, "string") + assert.strictEqual(result.elements, "Not human readable") + }) + + it("should return 'Not human readable' for collection with binary data", async () => { + const binaryData = "test\uFFFDdata" + const mockClient = createMockClient({ + TYPE: "list", + TTL: -1, + MEMORY: 150, + LLEN: 3, + LRANGE: [binaryData, "readable", "alsoreadable"], + }) + + const result = await getKeyInfo(mockClient as any, "mylist") + + assert.strictEqual(result.name, "mylist") + assert.strictEqual(result.type, "list") + assert.strictEqual(result.collectionSize, 3) + // getFullKeyInfo calls getHumanReadableElement on the array result + // which now properly filters each element in the array + assert.ok(Array.isArray(result.elements)) + const elements = result.elements as string[] + assert.strictEqual(elements.length, 3) + assert.strictEqual(elements[0], "Not human readable") // binary data + assert.strictEqual(elements[1], "readable") + assert.strictEqual(elements[2], "alsoreadable") + }) +}) + describe("deleteKey", () => { let mockWs: any let messages: string[] diff --git a/apps/server/src/__tests__/utils.test.ts b/apps/server/src/__tests__/utils.test.ts index b45649af..491cf36e 100644 --- a/apps/server/src/__tests__/utils.test.ts +++ b/apps/server/src/__tests__/utils.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it } from "node:test" import assert from "node:assert" -import { parseInfo, parseResponse, parseClusterInfo } from "../utils.ts" +import { parseInfo, parseResponse, parseClusterInfo, isHumanReadable, getHumanReadableElement } from "../utils.ts" describe("parseInfo", () => { it("should parse INFO response string into key-value pairs", () => { @@ -178,3 +178,297 @@ valkey_version:8.0.0`, assert.throws(() => parseClusterInfo(123 as any)) }) }) + +describe("isReadableString", () => { + // Note: isReadableString is not exported, so we test it indirectly through isHumanReadable + // These tests verify the readable string detection logic + + it("should return true for valid UTF-8 strings without replacement character", () => { + const result = (isHumanReadable as any)("Hello World") + assert.strictEqual(typeof result, "boolean") + }) + + it("should return false for strings containing U+FFFD replacement character", () => { + const stringWithReplacement = "Hello\uFFFDWorld" + const result = (isHumanReadable as any)(stringWithReplacement) + assert.strictEqual(result, false) + }) + + it("should return true for empty strings", () => { + const result = (isHumanReadable as any)("") + assert.strictEqual(result, true) + }) + + it("should return true for strings with various Unicode characters", () => { + const unicodeString = "Hello 世界 🌍" + // Note: This will fail isHumanReadable due to printable ratio, but tests the readable string part + const result = (isHumanReadable as any)(unicodeString) + assert.strictEqual(typeof result, "boolean") + }) + + it("should return false for binary data with U+FFFD", () => { + const binaryWithReplacement = "\x00\x01\uFFFD\x03" + const result = (isHumanReadable as any)(binaryWithReplacement) + assert.strictEqual(result, false) + }) +}) + +describe("getPrintableRatio", () => { + // Note: getPrintableRatio is not exported, so we test it indirectly through isHumanReadable + // These tests verify the printable ratio calculation logic + + it("should return 1.0 for empty string", () => { + const result = (isHumanReadable as any)("") + assert.strictEqual(result, true) // Empty string has ratio 1.0, passes threshold + }) + + it("should return 1.0 for 100% printable ASCII", () => { + const asciiString = "Hello World 123!" + const result = (isHumanReadable as any)(asciiString) + assert.strictEqual(result, true) // 100% printable, passes threshold + }) + + it("should return 0.5 for 50% printable characters", () => { + // 5 printable chars, 5 non-printable chars + const mixedString = "Hello\x00\x01\x02\x03\x04" + const result = (isHumanReadable as any)(mixedString) + assert.strictEqual(result, false) // 50% < 90% threshold + }) + + it("should count tabs (code 9) as printable", () => { + const stringWithTabs = "Hello\tWorld\t!" + const result = (isHumanReadable as any)(stringWithTabs) + assert.strictEqual(result, true) // All characters printable including tabs + }) + + it("should count line feeds (code 10) as printable", () => { + const stringWithLF = "Hello\nWorld\n!" + const result = (isHumanReadable as any)(stringWithLF) + assert.strictEqual(result, true) // All characters printable including LF + }) + + it("should count carriage returns (code 13) as printable", () => { + const stringWithCR = "Hello\rWorld\r!" + const result = (isHumanReadable as any)(stringWithCR) + assert.strictEqual(result, true) // All characters printable including CR + }) + + it("should return low ratio for binary data with control characters", () => { + const binaryData = "\x00\x01\x02\x03\x04\x05\x06\x07\x08" + const result = (isHumanReadable as any)(binaryData) + assert.strictEqual(result, false) // Very low printable ratio + }) + + it("should handle single character strings (edge case)", () => { + const singleChar = "A" + const result = (isHumanReadable as any)(singleChar) + assert.strictEqual(result, true) // 100% printable + + const singleNonPrintable = "\x00" + const result2 = (isHumanReadable as any)(singleNonPrintable) + assert.strictEqual(result2, false) // 0% printable + }) +}) + +describe("isHumanReadable", () => { + it("should return false for non-string number type", () => { + const result = (isHumanReadable as any)(123) + assert.strictEqual(result, false) + }) + + it("should return false for non-string null type", () => { + const result = (isHumanReadable as any)(null) + assert.strictEqual(result, false) + }) + + it("should return false for non-string undefined type", () => { + const result = (isHumanReadable as any)(undefined) + assert.strictEqual(result, false) + }) + + it("should return false for non-string object type", () => { + const result = (isHumanReadable as any)({ key: "value" }) + assert.strictEqual(result, false) + }) + + it("should return false for non-string array type", () => { + const result = (isHumanReadable as any)(["a", "b", "c"]) + assert.strictEqual(result, false) + }) + + it("should return true for string with 95% printable ratio", () => { + // 19 printable chars + 1 non-printable = 95% ratio + const highRatioString = "Hello World Test!!\x00" + const result = (isHumanReadable as any)(highRatioString) + assert.strictEqual(result, true) // 95% >= 90% threshold + }) + + it("should return false for string with 85% printable ratio", () => { + // Need to create string with exactly 85% printable + // 17 printable + 3 non-printable = 85% + const lowRatioString = "Hello World Test\x00\x01\x02" + const result = (isHumanReadable as any)(lowRatioString) + assert.strictEqual(result, false) // 85% < 90% threshold + }) + + it("should return true for string with exactly 90% printable ratio (boundary)", () => { + // 9 printable + 1 non-printable = 90% + const boundaryString = "HelloTest\x00" + const result = (isHumanReadable as any)(boundaryString) + assert.strictEqual(result, true) // 90% >= 90% threshold + }) + + it("should return false for string with 89.9% printable ratio (boundary)", () => { + // Need string where ratio is just below 90% + // 899 printable + 101 non-printable = 89.9% + const justBelowString = "A".repeat(899) + "\x00".repeat(101) + const result = (isHumanReadable as any)(justBelowString) + assert.strictEqual(result, false) // 89.9% < 90% threshold + }) + + it("should return false for string with U+FFFD even if high printable ratio", () => { + // String with replacement character should fail regardless of printable ratio + const stringWithReplacement = "Hello World Test\uFFFD" + const result = (isHumanReadable as any)(stringWithReplacement) + assert.strictEqual(result, false) // Contains U+FFFD + }) + + it("should return true for empty string", () => { + const result = (isHumanReadable as any)("") + assert.strictEqual(result, true) // Empty string is considered readable + }) + + it("should return true for very long readable string", () => { + const longString = "A".repeat(10000) + const result = (isHumanReadable as any)(longString) + assert.strictEqual(result, true) // 100% printable + }) + + it("should return true for mixed Unicode and ASCII readable", () => { + // Note: Unicode characters outside ASCII range are not counted as printable + // So this needs to be mostly ASCII to pass the 90% threshold + const mixedString = "Hello World! Test 123" + const result = (isHumanReadable as any)(mixedString) + assert.strictEqual(result, true) + }) + + it("should return false for binary data", () => { + const binaryData = "\x00\x01\x02\x03\x04\x05\x06\x07" + const result = (isHumanReadable as any)(binaryData) + assert.strictEqual(result, false) // 0% printable + }) +}) + +describe("getHumanReadableElement", () => { + it("should return original string for human-readable string", () => { + const readableString = "Hello World!" + const result = (getHumanReadableElement as any)(readableString) + assert.strictEqual(result, readableString) + }) + + it("should return 'Not human readable' for non-human-readable string", () => { + const binaryString = "\x00\x01\x02\x03\x04\x05" + const result = (getHumanReadableElement as any)(binaryString) + assert.strictEqual(result, "Not human readable") + }) + + it("should return 'Not human readable' for non-string number", () => { + const result = (getHumanReadableElement as any)(123) + assert.strictEqual(result, "Not human readable") + }) + + it("should return 'Not human readable' for non-string null", () => { + const result = (getHumanReadableElement as any)(null) + assert.strictEqual(result, "Not human readable") + }) + + it("should return 'Not human readable' for non-string object", () => { + const result = (getHumanReadableElement as any)({ key: "value" }) + assert.strictEqual(result, "Not human readable") + }) + + it("should return empty string for empty string", () => { + const result = (getHumanReadableElement as any)("") + assert.strictEqual(result, "") // Empty string is readable, returns itself + }) + + it("should return original string for string with exactly 90% printable", () => { + // 9 printable + 1 non-printable = 90% + const boundaryString = "HelloTest\x00" + const result = (getHumanReadableElement as any)(boundaryString) + assert.strictEqual(result, boundaryString) // At threshold, should pass + }) + + it("should return 'Not human readable' for binary data string", () => { + const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString("binary") + const result = (getHumanReadableElement as any)(binaryData) + assert.strictEqual(result, "Not human readable") + }) + + it("should handle flat arrays with readable strings", () => { + const input = ["item1", "item2", "item3"] + const result = (getHumanReadableElement as any)(input) + assert.ok(Array.isArray(result)) + assert.deepStrictEqual(result, ["item1", "item2", "item3"]) + }) + + it("should filter binary data in flat arrays", () => { + const binaryData = "test\uFFFDdata" + const input = ["readable", binaryData, "alsoreadable"] + const result = (getHumanReadableElement as any)(input) + assert.ok(Array.isArray(result)) + assert.deepStrictEqual(result, ["readable", "Not human readable", "alsoreadable"]) + }) + + it("should handle nested arrays (like streams)", () => { + const input = [ + ["id1", ["field1", "value1"]], + ["id2", ["field2", "value2"]], + ] + const result = (getHumanReadableElement as any)(input) + assert.ok(Array.isArray(result)) + assert.strictEqual(result.length, 2) + assert.ok(Array.isArray(result[0])) + assert.ok(Array.isArray(result[1])) + assert.deepStrictEqual(result, [ + ["id1", ["field1", "value1"]], + ["id2", ["field2", "value2"]], + ]) + }) + + it("should filter binary data in nested arrays", () => { + const binaryData = "test\uFFFDdata" + const input = [ + ["id1", ["readable", binaryData]], + ["id2", ["field", "value"]], + ] + const result = (getHumanReadableElement as any)(input) + assert.ok(Array.isArray(result)) + const nested = result as any[][] + assert.strictEqual(nested[0][0], "id1") + assert.ok(Array.isArray(nested[0][1])) + assert.strictEqual(nested[0][1][0], "readable") + assert.strictEqual(nested[0][1][1], "Not human readable") + assert.strictEqual(nested[1][0], "id2") + assert.deepStrictEqual(nested[1][1], ["field", "value"]) + }) + + it("should handle arrays with non-string primitives", () => { + const input = ["readable", 123, null, "alsoreadable"] + const result = (getHumanReadableElement as any)(input) + assert.ok(Array.isArray(result)) + assert.deepStrictEqual(result, [ + "readable", + "Not human readable", + "Not human readable", + "alsoreadable", + ]) + }) + + it("should handle empty arrays", () => { + const input: any[] = [] + const result = (getHumanReadableElement as any)(input) + assert.ok(Array.isArray(result)) + assert.strictEqual(result.length, 0) + }) +}) From fca9e104afb6a55aea180a8131f475a39d325383 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 17:36:52 -0700 Subject: [PATCH 04/11] uFFFD characters result in decoder error. Rework logic to account for that. Signed-off-by: Argus Li --- apps/server/src/keys-browser.ts | 4 +++ apps/server/src/utils.ts | 44 ++++++++++++++------------------- common/src/constants.ts | 4 +++ 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index 40141839..ee86f22b 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -100,6 +100,10 @@ async function getFullKeyInfo( } } catch (err) { console.log(`Could not get elements for key ${keyInfo.name}:`, err) + // Valkey client uses String decoder, which throws this error when it encounters non-UTF-8 bytes + if (err instanceof Error && err.message.includes("Decoding error")) { + return { ...keyInfo, elements: VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE } + } return keyInfo } } diff --git a/apps/server/src/utils.ts b/apps/server/src/utils.ts index 630d3427..51f38056 100644 --- a/apps/server/src/utils.ts +++ b/apps/server/src/utils.ts @@ -143,36 +143,25 @@ export async function isLastConnectedClusterNode( return clusterNodesMap.get(currentClusterId!)?.length === 1 } -const isReadableString = (str: string): boolean => { - return !str.includes('\uFFFD'); // Unicode replacement character when decoding fails -} +export const isHumanReadable = (str: string): boolean => { + if (str.length === 0) return true; + + let printableCount = 0; -const getPrintableRatio = (str: string): number => { - if (str.length === 0) return 1; + for (const char of str) { + const code = char.charCodeAt(0) - const printableCount = [...str].filter( - (char) => { - const code = char.charCodeAt(0) - - return ( + if ( (code >= 32 && code <= 126) // letters, numbers, punctuations || code === 9 // Tab || code === 10 // Line Feed || code === 13 // Carriage return - ) + ) { + printableCount++; } - ).length - - return printableCount / str.length; -} - -export const isHumanReadable = (data: GlideReturnType): boolean => { - if (typeof data !== 'string') { - return false; } - return isReadableString(data) - && getPrintableRatio(data) >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO; + return printableCount / str.length >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO; } export const getHumanReadableString = (str: string): string => { @@ -181,15 +170,20 @@ export const getHumanReadableString = (str: string): string => { : str; } -export type HumanReadableResult = string | HumanReadableResult[]; - -export const getHumanReadableElement = (element: GlideReturnType): HumanReadableResult => { +export const getHumanReadableElement = (element: GlideReturnType): GlideReturnType => { if (Array.isArray(element)) { return element.map((item) => getHumanReadableElement(item)); } else if (typeof element === "string") { return getHumanReadableString(element); + } else if (typeof element === "object" && element !== null) { + const result: Record = {}; + for (const [k, v] of Object.entries(element)) { + result[k] = typeof v === "number" || (typeof v === "string" && !isNaN(Number(v))) + ? Number(v) + : getHumanReadableElement(v); + } + return result; } - // For non-string, non-array types return VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE; } diff --git a/common/src/constants.ts b/common/src/constants.ts index fb9baff0..7d52439b 100644 --- a/common/src/constants.ts +++ b/common/src/constants.ts @@ -132,6 +132,10 @@ export const VALKEY_CLIENT = { } , KEY_VALUE_SIZE_LIMIT: 2048, // 2KiB HUMAN_READABLE: { + // Ratio of printable ASCII characters (letters, digits, punctuation, whitespace) + // required to consider a string human-readable. Tolerable ratio of occasional + // non-printable bytes that can appear in otherwise + // readable strings. ACCEPTABLE_PRINTABLE_RATIO: 0.90, NOT_READABLE_MESSAGE: "Not human readable", } From 6964ed14e12ed580c89cccc88ce4075277b42f9d Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 17:38:29 -0700 Subject: [PATCH 05/11] Update tests. Signed-off-by: Argus Li --- .../server/src/__tests__/keys-browser.test.ts | 53 +++--- apps/server/src/__tests__/utils.test.ts | 172 ++++-------------- 2 files changed, 58 insertions(+), 167 deletions(-) diff --git a/apps/server/src/__tests__/keys-browser.test.ts b/apps/server/src/__tests__/keys-browser.test.ts index 28f5bc76..9f5d21e3 100644 --- a/apps/server/src/__tests__/keys-browser.test.ts +++ b/apps/server/src/__tests__/keys-browser.test.ts @@ -61,7 +61,7 @@ describe("getKeyInfo", () => { describe("string keys with non-readable data", () => { it("should return 'Not human readable' for string with binary value", async () => { - const binaryValue = "test\uFFFDdata" + const binaryValue = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "string", TTL: -1, @@ -120,7 +120,7 @@ describe("getKeyInfo", () => { describe("hash keys with non-readable data", () => { it("should return 'Not human readable' for hash with binary values", async () => { - const binaryValue = "test\uFFFDdata" + const binaryValue = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "hash", TTL: -1, @@ -140,7 +140,7 @@ describe("getKeyInfo", () => { }) it("should filter mix of readable and non-readable hash values appropriately", async () => { - const binaryValue = "test\uFFFDdata" + const binaryValue = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "hash", TTL: -1, @@ -159,7 +159,7 @@ describe("getKeyInfo", () => { }) it("should return 'Not human readable' for hash with non-readable keys", async () => { - const binaryKey = "key\uFFFDtest" + const binaryKey = "key\x00\x01\x02test" const mockClient = createMockClient({ TYPE: "hash", TTL: -1, @@ -180,7 +180,7 @@ describe("getKeyInfo", () => { describe("set keys with non-readable data", () => { it("should return 'Not human readable' for set with binary data elements", async () => { - const binaryData = "test\uFFFDdata" + const binaryData = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "set", TTL: -1, @@ -200,7 +200,7 @@ describe("getKeyInfo", () => { }) it("should filter mix of readable and non-readable set elements appropriately", async () => { - const binaryData = "test\uFFFDdata" + const binaryData = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "set", TTL: -1, @@ -241,7 +241,7 @@ describe("getKeyInfo", () => { }) it("should filter binary data in list elements", async () => { - const binaryData = "test\uFFFDdata" + const binaryData = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "list", TTL: -1, @@ -270,7 +270,7 @@ describe("getKeyInfo", () => { TTL: -1, MEMORY: 200, ZCARD: 2, - ZRANGE: ["member1", "1.5", "member2", "2.5"], + ZRANGE: [{key: "member1", value: "1.5"}, {key: "member2", value: "2.5"}], }) const result = await getKeyInfo(mockClient as any, "myzset") @@ -279,18 +279,18 @@ describe("getKeyInfo", () => { assert.strictEqual(result.type, "zset") assert.strictEqual(result.collectionSize, 2) assert.ok(Array.isArray(result.elements)) - // ZRANGE with WITHSCORES returns [member1, score1, member2, score2, ...] - assert.deepStrictEqual(result.elements, ["member1", "1.5", "member2", "2.5"]) + // RESP3 ZRANGE with WITHSCORES returns [{key: member, value: score}, ...] + assert.deepStrictEqual(result.elements, [{key: "member1", value: 1.5}, {key: "member2", value: 2.5}]) }) it("should filter binary data in zset members", async () => { - const binaryMember = "member\uFFFDtest" + const binaryMember = "member\x00\x01\x02test" const mockClient = createMockClient({ TYPE: "zset", TTL: -1, MEMORY: 200, ZCARD: 3, - ZRANGE: ["readable", "1.0", binaryMember, "2.0", "alsoreadable", "3.0"], + ZRANGE: [{key: "readable", value: "1.0"}, {key: binaryMember, value: "2.0"}, {key: "alsoreadable", value: "3.0"}], }) const result = await getKeyInfo(mockClient as any, "myzset") @@ -299,13 +299,10 @@ describe("getKeyInfo", () => { assert.strictEqual(result.type, "zset") assert.strictEqual(result.collectionSize, 3) assert.ok(Array.isArray(result.elements)) - const elements = result.elements as string[] - assert.strictEqual(elements[0], "readable") - assert.strictEqual(elements[1], "1.0") - assert.strictEqual(elements[2], "Not human readable") - assert.strictEqual(elements[3], "2.0") - assert.strictEqual(elements[4], "alsoreadable") - assert.strictEqual(elements[5], "3.0") + const elements = result.elements as any[] + assert.deepStrictEqual(elements[0], {key: "readable", value: 1.0}) + assert.deepStrictEqual(elements[1], {key: "Not human readable", value: 2.0}) + assert.deepStrictEqual(elements[2], {key: "alsoreadable", value: 3.0}) }) }) @@ -336,8 +333,8 @@ describe("getKeyInfo", () => { }) it("should filter binary data in stream field names and values", async () => { - const binaryField = "field\uFFFDtest" - const binaryValue = "value\uFFFDtest" + const binaryField = "field\x00\x01\x02test" + const binaryValue = "value\x00\x01\x02test" const mockClient = createMockClient({ TYPE: "stream", TTL: -1, @@ -359,7 +356,7 @@ describe("getKeyInfo", () => { // First entry assert.strictEqual(elements[0][0], "1234567890-0") - const firstFields = elements[0][1] as string[] + const firstFields = elements[0][1] as unknown as string[] assert.strictEqual(firstFields[0], "readable_field") assert.strictEqual(firstFields[1], "readable_value") assert.strictEqual(firstFields[2], "Not human readable") @@ -367,7 +364,7 @@ describe("getKeyInfo", () => { // Second entry assert.strictEqual(elements[1][0], "1234567891-0") - const secondFields = elements[1][1] as string[] + const secondFields = elements[1][1] as unknown as string[] assert.strictEqual(secondFields[0], "field1") assert.strictEqual(secondFields[1], "Not human readable") }) @@ -391,7 +388,7 @@ describe("getKeyInfo", () => { }) it("should return 'Not human readable' for JSON with binary data", async () => { - const binaryJson = '{"name":"test\uFFFD","value":123}' + const binaryJson = '{"name":"test\x00\x01\x02\x03\x04\x05","value":123}' const mockClient = createMockClient({ TYPE: "rejson-rl", TTL: -1, @@ -427,7 +424,7 @@ describe("getKeyInfo", () => { describe("getScanKeyInfo", () => { it("should properly filter binary data in hash scan", async () => { - const binaryValue = "test\uFFFDdata" + const binaryValue = "test\x00\x01\x02data" const mockClient = { customCommand: mock.fn(async (cmd: string[]) => { if (cmd[0] === "TYPE") return "hash" @@ -450,7 +447,7 @@ describe("getScanKeyInfo", () => { }) it("should properly filter binary data in set scan", async () => { - const binaryData = "test\uFFFDdata" + const binaryData = "test\x00\x01\x02data" const mockClient = { customCommand: mock.fn(async (cmd: string[]) => { if (cmd[0] === "TYPE") return "set" @@ -476,7 +473,7 @@ describe("getScanKeyInfo", () => { describe("getFullKeyInfo", () => { it("should return 'Not human readable' for string value with binary data", async () => { - const binaryValue = "test\uFFFDdata" + const binaryValue = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "string", TTL: -1, @@ -492,7 +489,7 @@ describe("getFullKeyInfo", () => { }) it("should return 'Not human readable' for collection with binary data", async () => { - const binaryData = "test\uFFFDdata" + const binaryData = "test\x00\x01\x02data" const mockClient = createMockClient({ TYPE: "list", TTL: -1, diff --git a/apps/server/src/__tests__/utils.test.ts b/apps/server/src/__tests__/utils.test.ts index 491cf36e..7f70be0b 100644 --- a/apps/server/src/__tests__/utils.test.ts +++ b/apps/server/src/__tests__/utils.test.ts @@ -179,123 +179,7 @@ valkey_version:8.0.0`, }) }) -describe("isReadableString", () => { - // Note: isReadableString is not exported, so we test it indirectly through isHumanReadable - // These tests verify the readable string detection logic - - it("should return true for valid UTF-8 strings without replacement character", () => { - const result = (isHumanReadable as any)("Hello World") - assert.strictEqual(typeof result, "boolean") - }) - - it("should return false for strings containing U+FFFD replacement character", () => { - const stringWithReplacement = "Hello\uFFFDWorld" - const result = (isHumanReadable as any)(stringWithReplacement) - assert.strictEqual(result, false) - }) - - it("should return true for empty strings", () => { - const result = (isHumanReadable as any)("") - assert.strictEqual(result, true) - }) - - it("should return true for strings with various Unicode characters", () => { - const unicodeString = "Hello 世界 🌍" - // Note: This will fail isHumanReadable due to printable ratio, but tests the readable string part - const result = (isHumanReadable as any)(unicodeString) - assert.strictEqual(typeof result, "boolean") - }) - - it("should return false for binary data with U+FFFD", () => { - const binaryWithReplacement = "\x00\x01\uFFFD\x03" - const result = (isHumanReadable as any)(binaryWithReplacement) - assert.strictEqual(result, false) - }) -}) - -describe("getPrintableRatio", () => { - // Note: getPrintableRatio is not exported, so we test it indirectly through isHumanReadable - // These tests verify the printable ratio calculation logic - - it("should return 1.0 for empty string", () => { - const result = (isHumanReadable as any)("") - assert.strictEqual(result, true) // Empty string has ratio 1.0, passes threshold - }) - - it("should return 1.0 for 100% printable ASCII", () => { - const asciiString = "Hello World 123!" - const result = (isHumanReadable as any)(asciiString) - assert.strictEqual(result, true) // 100% printable, passes threshold - }) - - it("should return 0.5 for 50% printable characters", () => { - // 5 printable chars, 5 non-printable chars - const mixedString = "Hello\x00\x01\x02\x03\x04" - const result = (isHumanReadable as any)(mixedString) - assert.strictEqual(result, false) // 50% < 90% threshold - }) - - it("should count tabs (code 9) as printable", () => { - const stringWithTabs = "Hello\tWorld\t!" - const result = (isHumanReadable as any)(stringWithTabs) - assert.strictEqual(result, true) // All characters printable including tabs - }) - - it("should count line feeds (code 10) as printable", () => { - const stringWithLF = "Hello\nWorld\n!" - const result = (isHumanReadable as any)(stringWithLF) - assert.strictEqual(result, true) // All characters printable including LF - }) - - it("should count carriage returns (code 13) as printable", () => { - const stringWithCR = "Hello\rWorld\r!" - const result = (isHumanReadable as any)(stringWithCR) - assert.strictEqual(result, true) // All characters printable including CR - }) - - it("should return low ratio for binary data with control characters", () => { - const binaryData = "\x00\x01\x02\x03\x04\x05\x06\x07\x08" - const result = (isHumanReadable as any)(binaryData) - assert.strictEqual(result, false) // Very low printable ratio - }) - - it("should handle single character strings (edge case)", () => { - const singleChar = "A" - const result = (isHumanReadable as any)(singleChar) - assert.strictEqual(result, true) // 100% printable - - const singleNonPrintable = "\x00" - const result2 = (isHumanReadable as any)(singleNonPrintable) - assert.strictEqual(result2, false) // 0% printable - }) -}) - describe("isHumanReadable", () => { - it("should return false for non-string number type", () => { - const result = (isHumanReadable as any)(123) - assert.strictEqual(result, false) - }) - - it("should return false for non-string null type", () => { - const result = (isHumanReadable as any)(null) - assert.strictEqual(result, false) - }) - - it("should return false for non-string undefined type", () => { - const result = (isHumanReadable as any)(undefined) - assert.strictEqual(result, false) - }) - - it("should return false for non-string object type", () => { - const result = (isHumanReadable as any)({ key: "value" }) - assert.strictEqual(result, false) - }) - - it("should return false for non-string array type", () => { - const result = (isHumanReadable as any)(["a", "b", "c"]) - assert.strictEqual(result, false) - }) - it("should return true for string with 95% printable ratio", () => { // 19 printable chars + 1 non-printable = 95% ratio const highRatioString = "Hello World Test!!\x00" @@ -326,18 +210,26 @@ describe("isHumanReadable", () => { assert.strictEqual(result, false) // 89.9% < 90% threshold }) - it("should return false for string with U+FFFD even if high printable ratio", () => { - // String with replacement character should fail regardless of printable ratio - const stringWithReplacement = "Hello World Test\uFFFD" - const result = (isHumanReadable as any)(stringWithReplacement) - assert.strictEqual(result, false) // Contains U+FFFD - }) - it("should return true for empty string", () => { const result = (isHumanReadable as any)("") assert.strictEqual(result, true) // Empty string is considered readable }) + it("should count tabs as printable", () => { + const result = (isHumanReadable as any)("Hello\tWorld\t!") + assert.strictEqual(result, true) + }) + + it("should count line feeds as printable", () => { + const result = (isHumanReadable as any)("Hello\nWorld\n!") + assert.strictEqual(result, true) + }) + + it("should count carriage returns as printable", () => { + const result = (isHumanReadable as any)("Hello\rWorld\r!") + assert.strictEqual(result, true) + }) + it("should return true for very long readable string", () => { const longString = "A".repeat(10000) const result = (isHumanReadable as any)(longString) @@ -382,27 +274,29 @@ describe("getHumanReadableElement", () => { assert.strictEqual(result, "Not human readable") }) - it("should return 'Not human readable' for non-string object", () => { + it("should recursively process plain objects", () => { const result = (getHumanReadableElement as any)({ key: "value" }) - assert.strictEqual(result, "Not human readable") + assert.deepStrictEqual(result, { key: "value" }) }) - it("should return empty string for empty string", () => { - const result = (getHumanReadableElement as any)("") - assert.strictEqual(result, "") // Empty string is readable, returns itself + it("should filter binary key in {key, value} object", () => { + const result = (getHumanReadableElement as any)({ key: "\x00\x01\x02binary", value: "1.5" }) + assert.deepStrictEqual(result, { key: "Not human readable", value: 1.5 }) }) - it("should return original string for string with exactly 90% printable", () => { - // 9 printable + 1 non-printable = 90% - const boundaryString = "HelloTest\x00" - const result = (getHumanReadableElement as any)(boundaryString) - assert.strictEqual(result, boundaryString) // At threshold, should pass + it("should preserve numeric string values in objects", () => { + const result = (getHumanReadableElement as any)({ key: "member1", value: "2.5" }) + assert.deepStrictEqual(result, { key: "member1", value: 2.5 }) }) - it("should return 'Not human readable' for binary data string", () => { - const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString("binary") - const result = (getHumanReadableElement as any)(binaryData) - assert.strictEqual(result, "Not human readable") + it("should preserve number values in objects", () => { + const result = (getHumanReadableElement as any)({ key: "member1", value: 2.5 }) + assert.deepStrictEqual(result, { key: "member1", value: 2.5 }) + }) + + it("should filter non-numeric binary values in objects", () => { + const result = (getHumanReadableElement as any)({ key: "field", value: "\x00\x01\x02binary" }) + assert.deepStrictEqual(result, { key: "field", value: "Not human readable" }) }) it("should handle flat arrays with readable strings", () => { @@ -413,7 +307,7 @@ describe("getHumanReadableElement", () => { }) it("should filter binary data in flat arrays", () => { - const binaryData = "test\uFFFDdata" + const binaryData = "test\x00\x01\x02data" const input = ["readable", binaryData, "alsoreadable"] const result = (getHumanReadableElement as any)(input) assert.ok(Array.isArray(result)) @@ -437,7 +331,7 @@ describe("getHumanReadableElement", () => { }) it("should filter binary data in nested arrays", () => { - const binaryData = "test\uFFFDdata" + const binaryData = "test\x00\x01\x02data" const input = [ ["id1", ["readable", binaryData]], ["id2", ["field", "value"]], From 9f2bd29fe9231a0af11cdc3778ae2502fdfb22c7 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 18:27:26 -0700 Subject: [PATCH 06/11] Use Unicode property escape regex Signed-off-by: Argus Li --- apps/server/src/utils.ts | 38 +++++++++++++++++--------------------- common/src/constants.ts | 3 ++- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/server/src/utils.ts b/apps/server/src/utils.ts index 51f38056..eafaa462 100644 --- a/apps/server/src/utils.ts +++ b/apps/server/src/utils.ts @@ -144,46 +144,42 @@ export async function isLastConnectedClusterNode( } export const isHumanReadable = (str: string): boolean => { - if (str.length === 0) return true; + if (str.length === 0) return true - let printableCount = 0; + let printableCount = 0 + // UTF-16 characters count as 2 if using str.length. + let totalCount = 0 for (const char of str) { - const code = char.charCodeAt(0) - - if ( - (code >= 32 && code <= 126) // letters, numbers, punctuations - || code === 9 // Tab - || code === 10 // Line Feed - || code === 13 // Carriage return - ) { - printableCount++; + totalCount++ + if (VALKEY_CLIENT.HUMAN_READABLE.PRINTABLE_RE.test(char)) { + printableCount++ } } - return printableCount / str.length >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO; + return printableCount / totalCount >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO } export const getHumanReadableString = (str: string): string => { return !isHumanReadable(str) - ? VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE - : str; + ? VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE + : str } export const getHumanReadableElement = (element: GlideReturnType): GlideReturnType => { if (Array.isArray(element)) { - return element.map((item) => getHumanReadableElement(item)); + return element.map((item) => getHumanReadableElement(item)) } else if (typeof element === "string") { - return getHumanReadableString(element); + return getHumanReadableString(element) } else if (typeof element === "object" && element !== null) { - const result: Record = {}; + const result: Record = {} for (const [k, v] of Object.entries(element)) { result[k] = typeof v === "number" || (typeof v === "string" && !isNaN(Number(v))) - ? Number(v) - : getHumanReadableElement(v); + ? Number(v) + : getHumanReadableElement(v) } - return result; + return result } - return VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE; + return VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE } diff --git a/common/src/constants.ts b/common/src/constants.ts index 7d52439b..625312f5 100644 --- a/common/src/constants.ts +++ b/common/src/constants.ts @@ -132,13 +132,14 @@ export const VALKEY_CLIENT = { } , KEY_VALUE_SIZE_LIMIT: 2048, // 2KiB HUMAN_READABLE: { + PRINTABLE_RE: /^[\p{L}\p{N}\p{P}\p{S}\p{Z}\p{M}\t\n\r]$/u, // Ratio of printable ASCII characters (letters, digits, punctuation, whitespace) // required to consider a string human-readable. Tolerable ratio of occasional // non-printable bytes that can appear in otherwise // readable strings. ACCEPTABLE_PRINTABLE_RATIO: 0.90, NOT_READABLE_MESSAGE: "Not human readable", - } + }, } export const COMMANDLOG_TYPE = { SLOW: "slow", From d67cf214fddfa6fc2c882a268991b1f25a0212fa Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 18:29:25 -0700 Subject: [PATCH 07/11] Lint Signed-off-by: Argus Li --- .../server/src/__tests__/keys-browser.test.ts | 16 ++++---- apps/server/src/__tests__/utils.test.ts | 39 +++++++++++++++++-- apps/server/src/keys-browser.ts | 2 +- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/apps/server/src/__tests__/keys-browser.test.ts b/apps/server/src/__tests__/keys-browser.test.ts index 9f5d21e3..dd06c779 100644 --- a/apps/server/src/__tests__/keys-browser.test.ts +++ b/apps/server/src/__tests__/keys-browser.test.ts @@ -270,7 +270,7 @@ describe("getKeyInfo", () => { TTL: -1, MEMORY: 200, ZCARD: 2, - ZRANGE: [{key: "member1", value: "1.5"}, {key: "member2", value: "2.5"}], + ZRANGE: [{ key: "member1", value: "1.5" }, { key: "member2", value: "2.5" }], }) const result = await getKeyInfo(mockClient as any, "myzset") @@ -280,7 +280,7 @@ describe("getKeyInfo", () => { assert.strictEqual(result.collectionSize, 2) assert.ok(Array.isArray(result.elements)) // RESP3 ZRANGE with WITHSCORES returns [{key: member, value: score}, ...] - assert.deepStrictEqual(result.elements, [{key: "member1", value: 1.5}, {key: "member2", value: 2.5}]) + assert.deepStrictEqual(result.elements, [{ key: "member1", value: 1.5 }, { key: "member2", value: 2.5 }]) }) it("should filter binary data in zset members", async () => { @@ -290,7 +290,7 @@ describe("getKeyInfo", () => { TTL: -1, MEMORY: 200, ZCARD: 3, - ZRANGE: [{key: "readable", value: "1.0"}, {key: binaryMember, value: "2.0"}, {key: "alsoreadable", value: "3.0"}], + ZRANGE: [{ key: "readable", value: "1.0" }, { key: binaryMember, value: "2.0" }, { key: "alsoreadable", value: "3.0" }], }) const result = await getKeyInfo(mockClient as any, "myzset") @@ -300,9 +300,9 @@ describe("getKeyInfo", () => { assert.strictEqual(result.collectionSize, 3) assert.ok(Array.isArray(result.elements)) const elements = result.elements as any[] - assert.deepStrictEqual(elements[0], {key: "readable", value: 1.0}) - assert.deepStrictEqual(elements[1], {key: "Not human readable", value: 2.0}) - assert.deepStrictEqual(elements[2], {key: "alsoreadable", value: 3.0}) + assert.deepStrictEqual(elements[0], { key: "readable", value: 1.0 }) + assert.deepStrictEqual(elements[1], { key: "Not human readable", value: 2.0 }) + assert.deepStrictEqual(elements[2], { key: "alsoreadable", value: 3.0 }) }) }) @@ -372,7 +372,7 @@ describe("getKeyInfo", () => { describe("json keys", () => { it("should get json key info with readable JSON string", async () => { - const jsonValue = '{"name":"test","value":123}' + const jsonValue = "{\"name\":\"test\",\"value\":123}" const mockClient = createMockClient({ TYPE: "rejson-rl", TTL: -1, @@ -388,7 +388,7 @@ describe("getKeyInfo", () => { }) it("should return 'Not human readable' for JSON with binary data", async () => { - const binaryJson = '{"name":"test\x00\x01\x02\x03\x04\x05","value":123}' + const binaryJson = "{\"name\":\"test\x00\x01\x02\x03\x04\x05\",\"value\":123}" const mockClient = createMockClient({ TYPE: "rejson-rl", TTL: -1, diff --git a/apps/server/src/__tests__/utils.test.ts b/apps/server/src/__tests__/utils.test.ts index 7f70be0b..e2794ee9 100644 --- a/apps/server/src/__tests__/utils.test.ts +++ b/apps/server/src/__tests__/utils.test.ts @@ -237,13 +237,46 @@ describe("isHumanReadable", () => { }) it("should return true for mixed Unicode and ASCII readable", () => { - // Note: Unicode characters outside ASCII range are not counted as printable - // So this needs to be mostly ASCII to pass the 90% threshold - const mixedString = "Hello World! Test 123" + const mixedString = "Hello 你好 World!" const result = (isHumanReadable as any)(mixedString) assert.strictEqual(result, true) }) + it("should return true for Chinese characters", () => { + const result = (isHumanReadable as any)("你好世界") + assert.strictEqual(result, true) + }) + + it("should return true for Japanese characters", () => { + const result = (isHumanReadable as any)("こんにちは") + assert.strictEqual(result, true) + }) + + it("should return true for Korean characters", () => { + const result = (isHumanReadable as any)("안녕하세요") + assert.strictEqual(result, true) + }) + + it("should return true for Cyrillic characters", () => { + const result = (isHumanReadable as any)("Привет мир") + assert.strictEqual(result, true) + }) + + it("should return true for Arabic characters", () => { + const result = (isHumanReadable as any)("مرحبا بالعالم") + assert.strictEqual(result, true) + }) + + it("should return true for accented Latin characters", () => { + const result = (isHumanReadable as any)("café résumé naïve") + assert.strictEqual(result, true) + }) + + it("should return true for emoji", () => { + const result = (isHumanReadable as any)("Hello 🌍🎉👋") + assert.strictEqual(result, true) + }) + it("should return false for binary data", () => { const binaryData = "\x00\x01\x02\x03\x04\x05\x06\x07" const result = (isHumanReadable as any)(binaryData) diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index ee86f22b..b7f9f929 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -49,7 +49,7 @@ async function getScanKeyInfo( for (let i = 0; i < elements.length; i += 2){ results.add({ key: getHumanReadableString(elements[i] as string), - value: getHumanReadableString(elements[i + 1] as string) + value: getHumanReadableString(elements[i + 1] as string), }) } } else { From ec01dc9a62a6f22e2828501c1c8527e9147f0567 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 19:02:23 -0700 Subject: [PATCH 08/11] Add support for non-english printable characters. Signed-off-by: Argus Li --- apps/server/src/utils.ts | 16 +++++----------- common/src/constants.ts | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/server/src/utils.ts b/apps/server/src/utils.ts index eafaa462..f025cbec 100644 --- a/apps/server/src/utils.ts +++ b/apps/server/src/utils.ts @@ -146,18 +146,12 @@ export async function isLastConnectedClusterNode( export const isHumanReadable = (str: string): boolean => { if (str.length === 0) return true - let printableCount = 0 - // UTF-16 characters count as 2 if using str.length. - let totalCount = 0 - - for (const char of str) { - totalCount++ - if (VALKEY_CLIENT.HUMAN_READABLE.PRINTABLE_RE.test(char)) { - printableCount++ - } - } + // Count code points to handle surrogate pairs correctly + const totalCount = Array.from(str).length + + const nonPrintableCount = str.match(VALKEY_CLIENT.HUMAN_READABLE.NON_PRINTABLE_RE)?.length ?? 0 - return printableCount / totalCount >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO + return (totalCount - nonPrintableCount) / totalCount >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO } export const getHumanReadableString = (str: string): string => { diff --git a/common/src/constants.ts b/common/src/constants.ts index 625312f5..234fa8e6 100644 --- a/common/src/constants.ts +++ b/common/src/constants.ts @@ -132,7 +132,7 @@ export const VALKEY_CLIENT = { } , KEY_VALUE_SIZE_LIMIT: 2048, // 2KiB HUMAN_READABLE: { - PRINTABLE_RE: /^[\p{L}\p{N}\p{P}\p{S}\p{Z}\p{M}\t\n\r]$/u, + NON_PRINTABLE_RE: /[^\p{L}\p{N}\p{P}\p{S}\p{Z}\p{M}\t\n\r]/gu, // Ratio of printable ASCII characters (letters, digits, punctuation, whitespace) // required to consider a string human-readable. Tolerable ratio of occasional // non-printable bytes that can appear in otherwise From 600bc9b0a48e8aa928afdf0fbf6e139730519adc Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 9 Mar 2026 13:28:25 -0700 Subject: [PATCH 09/11] Add alternative elementsWarning. This alternative to elements is displayed if there's a reason why elements cannot be populated. Currently it's only if size is larger than the threshold. Signed-off-by: Argus Li --- .../key-browser/key-details/key-details.tsx | 121 ++++++++++-------- apps/server/src/keys-browser.ts | 3 +- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/apps/frontend/src/components/key-browser/key-details/key-details.tsx b/apps/frontend/src/components/key-browser/key-details/key-details.tsx index 972a694a..0df3c51b 100644 --- a/apps/frontend/src/components/key-browser/key-details/key-details.tsx +++ b/apps/frontend/src/components/key-browser/key-details/key-details.tsx @@ -23,6 +23,7 @@ interface BaseKeyInfo { ttl: number; size: number; collectionSize?: number; + elementsWarning?: string; } interface ElementInfo { @@ -155,68 +156,76 @@ export default function KeyDetails({ selectedKey, selectedKeyInfo, connectionId, - {/* show different key types */} - {selectedKeyInfo.type === "string" && ( - - )} + {selectedKeyInfo.elementsWarning ? ( +
+ {selectedKeyInfo.elementsWarning} +
+ ) : ( + <> + {/* show different key types */} + {selectedKeyInfo.type === "string" && ( + + )} - {selectedKeyInfo.type === "hash" && ( - - )} + {selectedKeyInfo.type === "hash" && ( + + )} - {selectedKeyInfo.type === "list" && ( - - )} + {selectedKeyInfo.type === "list" && ( + + )} - {selectedKeyInfo.type === "set" && ( - - )} + {selectedKeyInfo.type === "set" && ( + + )} - {selectedKeyInfo.type === "zset" && ( - - )} + {selectedKeyInfo.type === "zset" && ( + + )} - {selectedKeyInfo.type === "stream" && ( - - )} + {selectedKeyInfo.type === "stream" && ( + + )} - {selectedKeyInfo.type === "ReJSON-RL" && ( - + {selectedKeyInfo.type === "ReJSON-RL" && ( + + )} + )} ) : ( diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index b7f9f929..c1ddfc8e 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -23,6 +23,7 @@ interface EnrichedKeyInfo { collectionSize?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any elements?: any; // this can be array, object, or string depending on the key type. + elementsWarning?: string; // alternative for elements when they cannot be displayed. } async function getScanKeyInfo( @@ -154,7 +155,7 @@ export async function getKeyInfo( if (commands.sizeCmd){ keyInfo.collectionSize = await (client.customCommand([commands.sizeCmd, key])) as number } - keyInfo.elements = `This key is ${formatBytes(memoryUsage)}, which is larger than the maximum display size of ${formatBytes(VALKEY_CLIENT.KEY_VALUE_SIZE_LIMIT)}.` + keyInfo.elementsWarning = `This key is ${formatBytes(memoryUsage)}, which is larger than the maximum display size of ${formatBytes(VALKEY_CLIENT.KEY_VALUE_SIZE_LIMIT)}.` return keyInfo } From 8db1151037d1a3933626a2245abd612c43e70a4f Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 10 Mar 2026 10:27:15 -0700 Subject: [PATCH 10/11] Use elements warning. Signed-off-by: Argus Li --- apps/server/src/keys-browser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index c1ddfc8e..442c78b0 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -103,7 +103,7 @@ async function getFullKeyInfo( console.log(`Could not get elements for key ${keyInfo.name}:`, err) // Valkey client uses String decoder, which throws this error when it encounters non-UTF-8 bytes if (err instanceof Error && err.message.includes("Decoding error")) { - return { ...keyInfo, elements: VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE } + return { ...keyInfo, elementsWarning: VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE } } return keyInfo } From 9b27a92a72134a6b15d5838e3f49c4608fbb61c0 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 10 Mar 2026 11:29:35 -0700 Subject: [PATCH 11/11] Remove human readable ratio and update tests. Signed-off-by: Argus Li --- .../server/src/__tests__/keys-browser.test.ts | 322 +----------------- apps/server/src/__tests__/utils.test.ts | 222 +----------- apps/server/src/keys-browser.ts | 18 +- apps/server/src/utils.ts | 38 +-- common/src/constants.ts | 10 +- 5 files changed, 15 insertions(+), 595 deletions(-) diff --git a/apps/server/src/__tests__/keys-browser.test.ts b/apps/server/src/__tests__/keys-browser.test.ts index dd06c779..f5ea8f8e 100644 --- a/apps/server/src/__tests__/keys-browser.test.ts +++ b/apps/server/src/__tests__/keys-browser.test.ts @@ -59,40 +59,6 @@ describe("getKeyInfo", () => { assert.strictEqual(result.size, 0) }) - describe("string keys with non-readable data", () => { - it("should return 'Not human readable' for string with binary value", async () => { - const binaryValue = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "string", - TTL: -1, - MEMORY: 100, - GET: binaryValue, - }) - - const result = await getKeyInfo(mockClient as any, "mykey") - - assert.strictEqual(result.name, "mykey") - assert.strictEqual(result.type, "string") - assert.strictEqual(result.elements, "Not human readable") - }) - - it("should return original value for string at 90% printable threshold", async () => { - // 9 printable characters out of 10 = exactly 90% - const threshold90 = "abcdefghi\x00" - const mockClient = createMockClient({ - TYPE: "string", - TTL: -1, - MEMORY: 100, - GET: threshold90, - }) - - const result = await getKeyInfo(mockClient as any, "mykey") - - assert.strictEqual(result.name, "mykey") - assert.strictEqual(result.type, "string") - assert.strictEqual(result.elements, threshold90) - }) - }) }) describe("hash keys", () => { @@ -118,105 +84,6 @@ describe("getKeyInfo", () => { ]) }) - describe("hash keys with non-readable data", () => { - it("should return 'Not human readable' for hash with binary values", async () => { - const binaryValue = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "hash", - TTL: -1, - MEMORY: 200, - HLEN: 2, - HSCAN: ["0", ["field1", binaryValue, "field2", "abc\x00\x01\x02\x03\x04\x05\x06\x07\x08"]], - }) - - const result = await getKeyInfo(mockClient as any, "myhash") - - assert.strictEqual(result.name, "myhash") - assert.strictEqual(result.type, "hash") - assert.strictEqual(result.collectionSize, 2) - assert.ok(Array.isArray(result.elements)) - const elements = result.elements as Array<{ key: string; value: string }> - assert.ok(elements.some((e) => e.value === "Not human readable")) - }) - - it("should filter mix of readable and non-readable hash values appropriately", async () => { - const binaryValue = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "hash", - TTL: -1, - MEMORY: 200, - HLEN: 3, - HSCAN: ["0", ["field1", "readable", "field2", binaryValue, "field3", "alsoreadable"]], - }) - - const result = await getKeyInfo(mockClient as any, "myhash") - - assert.strictEqual(result.collectionSize, 3) - const elements = result.elements as Array<{ key: string; value: string }> - assert.ok(elements.some((e) => e.key === "field1" && e.value === "readable")) - assert.ok(elements.some((e) => e.key === "field2" && e.value === "Not human readable")) - assert.ok(elements.some((e) => e.key === "field3" && e.value === "alsoreadable")) - }) - - it("should return 'Not human readable' for hash with non-readable keys", async () => { - const binaryKey = "key\x00\x01\x02test" - const mockClient = createMockClient({ - TYPE: "hash", - TTL: -1, - MEMORY: 200, - HLEN: 2, - HSCAN: ["0", [binaryKey, "value1", "normalkey", "value2"]], - }) - - const result = await getKeyInfo(mockClient as any, "myhash") - - assert.strictEqual(result.collectionSize, 2) - const elements = result.elements as Array<{ key: string; value: string }> - assert.ok(elements.some((e) => e.key === "Not human readable")) - assert.ok(elements.some((e) => e.key === "normalkey" && e.value === "value2")) - }) - }) - }) - - describe("set keys with non-readable data", () => { - it("should return 'Not human readable' for set with binary data elements", async () => { - const binaryData = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "set", - TTL: -1, - MEMORY: 150, - SCARD: 2, - SSCAN: ["0", [binaryData, "abc\x00\x01\x02\x03\x04\x05\x06\x07\x08"]], - }) - - const result = await getKeyInfo(mockClient as any, "myset") - - assert.strictEqual(result.name, "myset") - assert.strictEqual(result.type, "set") - assert.strictEqual(result.collectionSize, 2) - assert.ok(Array.isArray(result.elements)) - const elements = result.elements as string[] - assert.ok(elements.includes("Not human readable")) - }) - - it("should filter mix of readable and non-readable set elements appropriately", async () => { - const binaryData = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "set", - TTL: -1, - MEMORY: 150, - SCARD: 3, - SSCAN: ["0", ["readable1", binaryData, "readable2"]], - }) - - const result = await getKeyInfo(mockClient as any, "myset") - - assert.strictEqual(result.collectionSize, 3) - const elements = result.elements as string[] - assert.ok(elements.includes("readable1")) - assert.ok(elements.includes("Not human readable")) - assert.ok(elements.includes("readable2")) - }) }) describe("list keys", () => { @@ -234,33 +101,10 @@ describe("getKeyInfo", () => { assert.strictEqual(result.name, "mylist") assert.strictEqual(result.type, "list") assert.strictEqual(result.collectionSize, 2) - // getFullKeyInfo calls getHumanReadableElement on array result - // which returns an array with each element filtered assert.ok(Array.isArray(result.elements)) assert.deepStrictEqual(result.elements, ["item1", "item2"]) }) - it("should filter binary data in list elements", async () => { - const binaryData = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "list", - TTL: -1, - MEMORY: 150, - LLEN: 3, - LRANGE: ["readable1", binaryData, "readable2"], - }) - - const result = await getKeyInfo(mockClient as any, "mylist") - - assert.strictEqual(result.name, "mylist") - assert.strictEqual(result.type, "list") - assert.strictEqual(result.collectionSize, 3) - assert.ok(Array.isArray(result.elements)) - const elements = result.elements as string[] - assert.strictEqual(elements[0], "readable1") - assert.strictEqual(elements[1], "Not human readable") - assert.strictEqual(elements[2], "readable2") - }) }) describe("zset keys", () => { @@ -280,30 +124,9 @@ describe("getKeyInfo", () => { assert.strictEqual(result.collectionSize, 2) assert.ok(Array.isArray(result.elements)) // RESP3 ZRANGE with WITHSCORES returns [{key: member, value: score}, ...] - assert.deepStrictEqual(result.elements, [{ key: "member1", value: 1.5 }, { key: "member2", value: 2.5 }]) + assert.deepStrictEqual(result.elements, [{ key: "member1", value: "1.5" }, { key: "member2", value: "2.5" }]) }) - it("should filter binary data in zset members", async () => { - const binaryMember = "member\x00\x01\x02test" - const mockClient = createMockClient({ - TYPE: "zset", - TTL: -1, - MEMORY: 200, - ZCARD: 3, - ZRANGE: [{ key: "readable", value: "1.0" }, { key: binaryMember, value: "2.0" }, { key: "alsoreadable", value: "3.0" }], - }) - - const result = await getKeyInfo(mockClient as any, "myzset") - - assert.strictEqual(result.name, "myzset") - assert.strictEqual(result.type, "zset") - assert.strictEqual(result.collectionSize, 3) - assert.ok(Array.isArray(result.elements)) - const elements = result.elements as any[] - assert.deepStrictEqual(elements[0], { key: "readable", value: 1.0 }) - assert.deepStrictEqual(elements[1], { key: "Not human readable", value: 2.0 }) - assert.deepStrictEqual(elements[2], { key: "alsoreadable", value: 3.0 }) - }) }) describe("stream keys", () => { @@ -332,42 +155,6 @@ describe("getKeyInfo", () => { assert.ok(Array.isArray(elements[1])) }) - it("should filter binary data in stream field names and values", async () => { - const binaryField = "field\x00\x01\x02test" - const binaryValue = "value\x00\x01\x02test" - const mockClient = createMockClient({ - TYPE: "stream", - TTL: -1, - MEMORY: 300, - XLEN: 2, - XRANGE: [ - ["1234567890-0", ["readable_field", "readable_value", binaryField, binaryValue]], - ["1234567891-0", ["field1", binaryValue]], - ], - }) - - const result = await getKeyInfo(mockClient as any, "mystream") - - assert.strictEqual(result.name, "mystream") - assert.strictEqual(result.type, "stream") - assert.strictEqual(result.collectionSize, 2) - assert.ok(Array.isArray(result.elements)) - const elements = result.elements as string[][] - - // First entry - assert.strictEqual(elements[0][0], "1234567890-0") - const firstFields = elements[0][1] as unknown as string[] - assert.strictEqual(firstFields[0], "readable_field") - assert.strictEqual(firstFields[1], "readable_value") - assert.strictEqual(firstFields[2], "Not human readable") - assert.strictEqual(firstFields[3], "Not human readable") - - // Second entry - assert.strictEqual(elements[1][0], "1234567891-0") - const secondFields = elements[1][1] as unknown as string[] - assert.strictEqual(secondFields[0], "field1") - assert.strictEqual(secondFields[1], "Not human readable") - }) }) describe("json keys", () => { @@ -387,21 +174,6 @@ describe("getKeyInfo", () => { assert.strictEqual(result.elements, jsonValue) }) - it("should return 'Not human readable' for JSON with binary data", async () => { - const binaryJson = "{\"name\":\"test\x00\x01\x02\x03\x04\x05\",\"value\":123}" - const mockClient = createMockClient({ - TYPE: "rejson-rl", - TTL: -1, - MEMORY: 100, - "JSON.GET": binaryJson, - }) - - const result = await getKeyInfo(mockClient as any, "myjson") - - assert.strictEqual(result.name, "myjson") - assert.strictEqual(result.type, "rejson-rl") - assert.strictEqual(result.elements, "Not human readable") - }) }) describe("error handling", () => { @@ -422,98 +194,6 @@ describe("getKeyInfo", () => { }) }) -describe("getScanKeyInfo", () => { - it("should properly filter binary data in hash scan", async () => { - const binaryValue = "test\x00\x01\x02data" - const mockClient = { - customCommand: mock.fn(async (cmd: string[]) => { - if (cmd[0] === "TYPE") return "hash" - if (cmd[0] === "TTL") return -1 - if (cmd[0] === "MEMORY") return 200 - if (cmd[0] === "HLEN") return 2 - if (cmd[0] === "HSCAN") { - return ["0", ["field1", "readable", "field2", binaryValue]] - } - return null - }), - } - - const result = await getKeyInfo(mockClient as any, "myhash") - - assert.strictEqual(result.collectionSize, 2) - const elements = result.elements as Array<{ key: string; value: string }> - assert.ok(elements.some((e) => e.key === "field1" && e.value === "readable")) - assert.ok(elements.some((e) => e.key === "field2" && e.value === "Not human readable")) - }) - - it("should properly filter binary data in set scan", async () => { - const binaryData = "test\x00\x01\x02data" - const mockClient = { - customCommand: mock.fn(async (cmd: string[]) => { - if (cmd[0] === "TYPE") return "set" - if (cmd[0] === "TTL") return -1 - if (cmd[0] === "MEMORY") return 150 - if (cmd[0] === "SCARD") return 3 - if (cmd[0] === "SSCAN") { - return ["0", ["readable1", binaryData, "readable2"]] - } - return null - }), - } - - const result = await getKeyInfo(mockClient as any, "myset") - - assert.strictEqual(result.collectionSize, 3) - const elements = result.elements as string[] - assert.ok(elements.includes("readable1")) - assert.ok(elements.includes("Not human readable")) - assert.ok(elements.includes("readable2")) - }) -}) - -describe("getFullKeyInfo", () => { - it("should return 'Not human readable' for string value with binary data", async () => { - const binaryValue = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "string", - TTL: -1, - MEMORY: 100, - GET: binaryValue, - }) - - const result = await getKeyInfo(mockClient as any, "mykey") - - assert.strictEqual(result.name, "mykey") - assert.strictEqual(result.type, "string") - assert.strictEqual(result.elements, "Not human readable") - }) - - it("should return 'Not human readable' for collection with binary data", async () => { - const binaryData = "test\x00\x01\x02data" - const mockClient = createMockClient({ - TYPE: "list", - TTL: -1, - MEMORY: 150, - LLEN: 3, - LRANGE: [binaryData, "readable", "alsoreadable"], - }) - - const result = await getKeyInfo(mockClient as any, "mylist") - - assert.strictEqual(result.name, "mylist") - assert.strictEqual(result.type, "list") - assert.strictEqual(result.collectionSize, 3) - // getFullKeyInfo calls getHumanReadableElement on the array result - // which now properly filters each element in the array - assert.ok(Array.isArray(result.elements)) - const elements = result.elements as string[] - assert.strictEqual(elements.length, 3) - assert.strictEqual(elements[0], "Not human readable") // binary data - assert.strictEqual(elements[1], "readable") - assert.strictEqual(elements[2], "alsoreadable") - }) -}) - describe("deleteKey", () => { let mockWs: any let messages: string[] diff --git a/apps/server/src/__tests__/utils.test.ts b/apps/server/src/__tests__/utils.test.ts index e2794ee9..0692e383 100644 --- a/apps/server/src/__tests__/utils.test.ts +++ b/apps/server/src/__tests__/utils.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it } from "node:test" import assert from "node:assert" -import { parseInfo, parseResponse, parseClusterInfo, isHumanReadable, getHumanReadableElement } from "../utils.ts" +import { parseInfo, parseResponse, parseClusterInfo } from "../utils.ts" describe("parseInfo", () => { it("should parse INFO response string into key-value pairs", () => { @@ -179,223 +179,3 @@ valkey_version:8.0.0`, }) }) -describe("isHumanReadable", () => { - it("should return true for string with 95% printable ratio", () => { - // 19 printable chars + 1 non-printable = 95% ratio - const highRatioString = "Hello World Test!!\x00" - const result = (isHumanReadable as any)(highRatioString) - assert.strictEqual(result, true) // 95% >= 90% threshold - }) - - it("should return false for string with 85% printable ratio", () => { - // Need to create string with exactly 85% printable - // 17 printable + 3 non-printable = 85% - const lowRatioString = "Hello World Test\x00\x01\x02" - const result = (isHumanReadable as any)(lowRatioString) - assert.strictEqual(result, false) // 85% < 90% threshold - }) - - it("should return true for string with exactly 90% printable ratio (boundary)", () => { - // 9 printable + 1 non-printable = 90% - const boundaryString = "HelloTest\x00" - const result = (isHumanReadable as any)(boundaryString) - assert.strictEqual(result, true) // 90% >= 90% threshold - }) - - it("should return false for string with 89.9% printable ratio (boundary)", () => { - // Need string where ratio is just below 90% - // 899 printable + 101 non-printable = 89.9% - const justBelowString = "A".repeat(899) + "\x00".repeat(101) - const result = (isHumanReadable as any)(justBelowString) - assert.strictEqual(result, false) // 89.9% < 90% threshold - }) - - it("should return true for empty string", () => { - const result = (isHumanReadable as any)("") - assert.strictEqual(result, true) // Empty string is considered readable - }) - - it("should count tabs as printable", () => { - const result = (isHumanReadable as any)("Hello\tWorld\t!") - assert.strictEqual(result, true) - }) - - it("should count line feeds as printable", () => { - const result = (isHumanReadable as any)("Hello\nWorld\n!") - assert.strictEqual(result, true) - }) - - it("should count carriage returns as printable", () => { - const result = (isHumanReadable as any)("Hello\rWorld\r!") - assert.strictEqual(result, true) - }) - - it("should return true for very long readable string", () => { - const longString = "A".repeat(10000) - const result = (isHumanReadable as any)(longString) - assert.strictEqual(result, true) // 100% printable - }) - - it("should return true for mixed Unicode and ASCII readable", () => { - const mixedString = "Hello 你好 World!" - const result = (isHumanReadable as any)(mixedString) - assert.strictEqual(result, true) - }) - - it("should return true for Chinese characters", () => { - const result = (isHumanReadable as any)("你好世界") - assert.strictEqual(result, true) - }) - - it("should return true for Japanese characters", () => { - const result = (isHumanReadable as any)("こんにちは") - assert.strictEqual(result, true) - }) - - it("should return true for Korean characters", () => { - const result = (isHumanReadable as any)("안녕하세요") - assert.strictEqual(result, true) - }) - - it("should return true for Cyrillic characters", () => { - const result = (isHumanReadable as any)("Привет мир") - assert.strictEqual(result, true) - }) - - it("should return true for Arabic characters", () => { - const result = (isHumanReadable as any)("مرحبا بالعالم") - assert.strictEqual(result, true) - }) - - it("should return true for accented Latin characters", () => { - const result = (isHumanReadable as any)("café résumé naïve") - assert.strictEqual(result, true) - }) - - it("should return true for emoji", () => { - const result = (isHumanReadable as any)("Hello 🌍🎉👋") - assert.strictEqual(result, true) - }) - - it("should return false for binary data", () => { - const binaryData = "\x00\x01\x02\x03\x04\x05\x06\x07" - const result = (isHumanReadable as any)(binaryData) - assert.strictEqual(result, false) // 0% printable - }) -}) - -describe("getHumanReadableElement", () => { - it("should return original string for human-readable string", () => { - const readableString = "Hello World!" - const result = (getHumanReadableElement as any)(readableString) - assert.strictEqual(result, readableString) - }) - - it("should return 'Not human readable' for non-human-readable string", () => { - const binaryString = "\x00\x01\x02\x03\x04\x05" - const result = (getHumanReadableElement as any)(binaryString) - assert.strictEqual(result, "Not human readable") - }) - - it("should return 'Not human readable' for non-string number", () => { - const result = (getHumanReadableElement as any)(123) - assert.strictEqual(result, "Not human readable") - }) - - it("should return 'Not human readable' for non-string null", () => { - const result = (getHumanReadableElement as any)(null) - assert.strictEqual(result, "Not human readable") - }) - - it("should recursively process plain objects", () => { - const result = (getHumanReadableElement as any)({ key: "value" }) - assert.deepStrictEqual(result, { key: "value" }) - }) - - it("should filter binary key in {key, value} object", () => { - const result = (getHumanReadableElement as any)({ key: "\x00\x01\x02binary", value: "1.5" }) - assert.deepStrictEqual(result, { key: "Not human readable", value: 1.5 }) - }) - - it("should preserve numeric string values in objects", () => { - const result = (getHumanReadableElement as any)({ key: "member1", value: "2.5" }) - assert.deepStrictEqual(result, { key: "member1", value: 2.5 }) - }) - - it("should preserve number values in objects", () => { - const result = (getHumanReadableElement as any)({ key: "member1", value: 2.5 }) - assert.deepStrictEqual(result, { key: "member1", value: 2.5 }) - }) - - it("should filter non-numeric binary values in objects", () => { - const result = (getHumanReadableElement as any)({ key: "field", value: "\x00\x01\x02binary" }) - assert.deepStrictEqual(result, { key: "field", value: "Not human readable" }) - }) - - it("should handle flat arrays with readable strings", () => { - const input = ["item1", "item2", "item3"] - const result = (getHumanReadableElement as any)(input) - assert.ok(Array.isArray(result)) - assert.deepStrictEqual(result, ["item1", "item2", "item3"]) - }) - - it("should filter binary data in flat arrays", () => { - const binaryData = "test\x00\x01\x02data" - const input = ["readable", binaryData, "alsoreadable"] - const result = (getHumanReadableElement as any)(input) - assert.ok(Array.isArray(result)) - assert.deepStrictEqual(result, ["readable", "Not human readable", "alsoreadable"]) - }) - - it("should handle nested arrays (like streams)", () => { - const input = [ - ["id1", ["field1", "value1"]], - ["id2", ["field2", "value2"]], - ] - const result = (getHumanReadableElement as any)(input) - assert.ok(Array.isArray(result)) - assert.strictEqual(result.length, 2) - assert.ok(Array.isArray(result[0])) - assert.ok(Array.isArray(result[1])) - assert.deepStrictEqual(result, [ - ["id1", ["field1", "value1"]], - ["id2", ["field2", "value2"]], - ]) - }) - - it("should filter binary data in nested arrays", () => { - const binaryData = "test\x00\x01\x02data" - const input = [ - ["id1", ["readable", binaryData]], - ["id2", ["field", "value"]], - ] - const result = (getHumanReadableElement as any)(input) - assert.ok(Array.isArray(result)) - const nested = result as any[][] - assert.strictEqual(nested[0][0], "id1") - assert.ok(Array.isArray(nested[0][1])) - assert.strictEqual(nested[0][1][0], "readable") - assert.strictEqual(nested[0][1][1], "Not human readable") - assert.strictEqual(nested[1][0], "id2") - assert.deepStrictEqual(nested[1][1], ["field", "value"]) - }) - - it("should handle arrays with non-string primitives", () => { - const input = ["readable", 123, null, "alsoreadable"] - const result = (getHumanReadableElement as any)(input) - assert.ok(Array.isArray(result)) - assert.deepStrictEqual(result, [ - "readable", - "Not human readable", - "Not human readable", - "alsoreadable", - ]) - }) - - it("should handle empty arrays", () => { - const input: any[] = [] - const result = (getHumanReadableElement as any)(input) - assert.ok(Array.isArray(result)) - assert.strictEqual(result.length, 0) - }) -}) diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index 442c78b0..93319dc9 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -13,7 +13,6 @@ import pLimit from "p-limit" import { VALKEY, VALKEY_CLIENT } from "../../../common/src/constants.ts" import { buildScanCommandArgs } from "./valkey-client-commands.ts" import { formatBytes } from "../../../common/src/bytes-conversion.ts" -import { getHumanReadableElement, getHumanReadableString } from "./utils.ts" interface EnrichedKeyInfo { name: string; @@ -49,12 +48,12 @@ async function getScanKeyInfo( // i.e. converting [key1, value1...] to [{key: key1, value}] for (let i = 0; i < elements.length; i += 2){ results.add({ - key: getHumanReadableString(elements[i] as string), - value: getHumanReadableString(elements[i + 1] as string), + key: elements[i] as string, + value: elements[i + 1] as string, }) } } else { - elements.forEach((element) => results.add(getHumanReadableString(element as string))) + elements.forEach((element) => results.add(element as string)) } cursor = newCursor } while (cursor !== "0") @@ -68,7 +67,10 @@ async function getScanKeyInfo( } } catch (err) { console.log(`Could not get elements for key ${keyInfo.name}:`, err) - return keyInfo + return { + ...keyInfo, + elementsWarning: VALKEY_CLIENT.MESSAGES.NOT_READABLE, + } } } @@ -90,20 +92,20 @@ async function getFullKeyInfo( return { ...keyInfo, collectionSize: results[1] as number, - elements: getHumanReadableElement(results[0]), + elements: results[0], } } else { // in case of string with no collectionSize return { ...keyInfo, - elements: getHumanReadableElement(results[0]), + elements: results[0], } } } catch (err) { console.log(`Could not get elements for key ${keyInfo.name}:`, err) // Valkey client uses String decoder, which throws this error when it encounters non-UTF-8 bytes if (err instanceof Error && err.message.includes("Decoding error")) { - return { ...keyInfo, elementsWarning: VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE } + return { ...keyInfo, elementsWarning: VALKEY_CLIENT.MESSAGES.NOT_READABLE } } return keyInfo } diff --git a/apps/server/src/utils.ts b/apps/server/src/utils.ts index f025cbec..3813823d 100644 --- a/apps/server/src/utils.ts +++ b/apps/server/src/utils.ts @@ -1,8 +1,7 @@ -import { ClusterResponse, GlideClient, GlideClusterClient, GlideReturnType } from "@valkey/valkey-glide" +import { ClusterResponse, GlideClient, GlideClusterClient } from "@valkey/valkey-glide" import * as R from "ramda" import { lookup, reverse } from "node:dns/promises" import { sanitizeUrl } from "../../../common/src/url-utils" -import { VALKEY_CLIENT } from "../../../common/src/constants" export const dns = { lookup, @@ -142,38 +141,3 @@ export async function isLastConnectedClusterNode( const currentClusterId = connection?.clusterId return clusterNodesMap.get(currentClusterId!)?.length === 1 } - -export const isHumanReadable = (str: string): boolean => { - if (str.length === 0) return true - - // Count code points to handle surrogate pairs correctly - const totalCount = Array.from(str).length - - const nonPrintableCount = str.match(VALKEY_CLIENT.HUMAN_READABLE.NON_PRINTABLE_RE)?.length ?? 0 - - return (totalCount - nonPrintableCount) / totalCount >= VALKEY_CLIENT.HUMAN_READABLE.ACCEPTABLE_PRINTABLE_RATIO -} - -export const getHumanReadableString = (str: string): string => { - return !isHumanReadable(str) - ? VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE - : str -} - -export const getHumanReadableElement = (element: GlideReturnType): GlideReturnType => { - if (Array.isArray(element)) { - return element.map((item) => getHumanReadableElement(item)) - } else if (typeof element === "string") { - return getHumanReadableString(element) - } else if (typeof element === "object" && element !== null) { - const result: Record = {} - for (const [k, v] of Object.entries(element)) { - result[k] = typeof v === "number" || (typeof v === "string" && !isNaN(Number(v))) - ? Number(v) - : getHumanReadableElement(v) - } - return result - } - - return VALKEY_CLIENT.HUMAN_READABLE.NOT_READABLE_MESSAGE -} diff --git a/common/src/constants.ts b/common/src/constants.ts index 234fa8e6..d82ec70a 100644 --- a/common/src/constants.ts +++ b/common/src/constants.ts @@ -131,14 +131,8 @@ export const VALKEY_CLIENT = { defaultCount: 50, } , KEY_VALUE_SIZE_LIMIT: 2048, // 2KiB - HUMAN_READABLE: { - NON_PRINTABLE_RE: /[^\p{L}\p{N}\p{P}\p{S}\p{Z}\p{M}\t\n\r]/gu, - // Ratio of printable ASCII characters (letters, digits, punctuation, whitespace) - // required to consider a string human-readable. Tolerable ratio of occasional - // non-printable bytes that can appear in otherwise - // readable strings. - ACCEPTABLE_PRINTABLE_RATIO: 0.90, - NOT_READABLE_MESSAGE: "Not human readable", + MESSAGES: { + NOT_READABLE: "Not human readable.", }, } export const COMMANDLOG_TYPE = {