|
|
@@ -143,6 +143,7 @@ export class RedisInspectorService {
|
|
|
): Promise<RedisInspectorRecordDetail> {
|
|
|
const byteLen = await this.redisService.strLen(key);
|
|
|
const rawValue = (await this.redisService.get(key)) ?? '';
|
|
|
+ const trimmedValue = rawValue.trim();
|
|
|
const { text, truncated } = this.prepareStringPreview(rawValue);
|
|
|
const meta: Record<string, any> = { byteLen };
|
|
|
const preview: Record<string, any> = {
|
|
|
@@ -150,6 +151,34 @@ export class RedisInspectorService {
|
|
|
data: text,
|
|
|
};
|
|
|
|
|
|
+ if (byteLen > MAX_JSON_PARSE_BYTES && trimmedValue.startsWith('[')) {
|
|
|
+ const extracted = this.extractFirstJsonArrayElements(trimmedValue, 5);
|
|
|
+ if (extracted && extracted.length) {
|
|
|
+ preview.format = 'json';
|
|
|
+ preview.data = extracted.map((item) => {
|
|
|
+ try {
|
|
|
+ return JSON.parse(item);
|
|
|
+ } catch {
|
|
|
+ return item;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const totalItems = this.countTopLevelJsonArrayElements(trimmedValue);
|
|
|
+ if (totalItems !== null) {
|
|
|
+ meta.itemCount = totalItems;
|
|
|
+ preview.truncated = totalItems > extracted.length;
|
|
|
+ } else {
|
|
|
+ preview.truncated = true;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ key,
|
|
|
+ type: 'string',
|
|
|
+ ttlSec,
|
|
|
+ meta,
|
|
|
+ preview,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
const canParseJson = byteLen <= MAX_JSON_PARSE_BYTES;
|
|
|
const candidate = canParseJson ? this.tryParseJson(rawValue) : undefined;
|
|
|
|
|
|
@@ -187,6 +216,134 @@ export class RedisInspectorService {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ private countTopLevelJsonArrayElements(raw: string): number | null {
|
|
|
+ const trimmed = raw.trim();
|
|
|
+ if (!trimmed.startsWith('[')) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ let depth = 0;
|
|
|
+ let inString = false;
|
|
|
+ let escape = false;
|
|
|
+ let seenNonWhitespace = false;
|
|
|
+ let count = 0;
|
|
|
+ for (let i = 1; i < trimmed.length; i++) {
|
|
|
+ const char = trimmed[i];
|
|
|
+ if (inString) {
|
|
|
+ if (escape) {
|
|
|
+ escape = false;
|
|
|
+ } else if (char === '\\') {
|
|
|
+ escape = true;
|
|
|
+ } else if (char === '"') {
|
|
|
+ inString = false;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (char === '"') {
|
|
|
+ inString = true;
|
|
|
+ seenNonWhitespace = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (char === '{' || char === '[') {
|
|
|
+ depth++;
|
|
|
+ seenNonWhitespace = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (char === '}' || char === ']') {
|
|
|
+ if (depth > 0) {
|
|
|
+ depth--;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (depth === 0 && (char === ',' || char === ']')) {
|
|
|
+ if (seenNonWhitespace) {
|
|
|
+ count++;
|
|
|
+ seenNonWhitespace = false;
|
|
|
+ }
|
|
|
+ if (char === ']') {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!/\s/.test(char)) {
|
|
|
+ seenNonWhitespace = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ private extractFirstJsonArrayElements(
|
|
|
+ raw: string,
|
|
|
+ maxItems: number,
|
|
|
+ ): string[] | null {
|
|
|
+ if (!raw || !raw.startsWith('[')) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const elements: string[] = [];
|
|
|
+ let depth = 0;
|
|
|
+ let inString = false;
|
|
|
+ let escape = false;
|
|
|
+ let elementStart: number | null = null;
|
|
|
+ for (let i = 1; i < raw.length && elements.length < maxItems; i++) {
|
|
|
+ const char = raw[i];
|
|
|
+ if (elementStart === null) {
|
|
|
+ if (/\s/.test(char)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (char === ']') {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ elementStart = i;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (inString) {
|
|
|
+ if (escape) {
|
|
|
+ escape = false;
|
|
|
+ } else if (char === '\\') {
|
|
|
+ escape = true;
|
|
|
+ } else if (char === '"') {
|
|
|
+ inString = false;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (char === '"') {
|
|
|
+ inString = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (char === '{' || char === '[') {
|
|
|
+ depth++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (char === '}' || char === ']') {
|
|
|
+ if (depth > 0) {
|
|
|
+ depth--;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((char === ',' || char === ']') && depth === 0) {
|
|
|
+ const segment = raw.slice(elementStart, i).trim();
|
|
|
+ if (segment) {
|
|
|
+ elements.push(segment);
|
|
|
+ }
|
|
|
+ elementStart = null;
|
|
|
+ if (char === ']') {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return elements.length ? elements : null;
|
|
|
+ }
|
|
|
+
|
|
|
private async inspectList(
|
|
|
key: string,
|
|
|
ttlSec: number,
|