feat(desktop): unify frontend error notices
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -37,6 +37,14 @@ import {
|
||||
} from "./components/ui/dialog";
|
||||
import { Input } from "./components/ui/input";
|
||||
import { Textarea } from "./components/ui/textarea";
|
||||
import {
|
||||
toBackendError,
|
||||
type BackendErrorContext,
|
||||
createAccentNotice,
|
||||
createNeutralNotice,
|
||||
toBackendErrorNotice,
|
||||
type OperatorNotice,
|
||||
} from "./lib/operator-notices";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
@@ -107,12 +115,6 @@ type BackendBootstrap = {
|
||||
next_backend_milestone: string;
|
||||
};
|
||||
|
||||
type BackendError = {
|
||||
code: string;
|
||||
message: string;
|
||||
detail: string | null;
|
||||
};
|
||||
|
||||
type RedisConnectionRequest = {
|
||||
target: ConnectionProfile["target"];
|
||||
password: string | null;
|
||||
@@ -375,7 +377,11 @@ function App() {
|
||||
const [consoleInput, setConsoleInput] = useState("GET cache:homepage");
|
||||
const [consoleHistory, setConsoleHistory] = useState(initialConsoleHistory);
|
||||
const [bootstrap, setBootstrap] = useState(fallbackBootstrap);
|
||||
const [banner, setBanner] = useState("String edits stay local to the shell until the backend write contract is wired.");
|
||||
const [banner, setBanner] = useState<OperatorNotice>(
|
||||
createNeutralNotice(
|
||||
"String edits stay local to the shell until the backend write contract is wired.",
|
||||
),
|
||||
);
|
||||
const [connectionDialogOpen, setConnectionDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isConnectionTestRunning, setIsConnectionTestRunning] = useState(false);
|
||||
@@ -403,6 +409,10 @@ function App() {
|
||||
const appVersion = __APP_VERSION__;
|
||||
const buildMode = import.meta.env.DEV ? "dev" : "production";
|
||||
const runtimeMode = tauriAvailable ? "live desktop bridge" : "browser demo fallback";
|
||||
const showNeutralBanner = (message: string) => setBanner(createNeutralNotice(message));
|
||||
const showAccentBanner = (message: string) => setBanner(createAccentNotice(message));
|
||||
const showBackendErrorBanner = (error: unknown, context: BackendErrorContext) =>
|
||||
setBanner(toBackendErrorNotice(error, context));
|
||||
|
||||
const activeConnection =
|
||||
connections.find((connection) => connection.id === activeConnectionId) ??
|
||||
@@ -467,7 +477,7 @@ function App() {
|
||||
`latestCommand=${latestConsoleEntry?.command ?? "none"}`,
|
||||
`latestCommandSource=${latestConsoleEntry?.source ?? "none"}`,
|
||||
`latestCommandStatus=${latestConsoleEntry?.status ?? "none"}`,
|
||||
`banner=${banner}`,
|
||||
`banner=${banner.message}`,
|
||||
].join("\n");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -558,7 +568,6 @@ function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const backendError = toBackendError(error);
|
||||
startTransition(() => {
|
||||
setLiveKeys([]);
|
||||
setLiveNextCursor("0");
|
||||
@@ -568,7 +577,11 @@ function App() {
|
||||
setDraftValue("");
|
||||
setTtlDraft("");
|
||||
setIsBrowseLoading(false);
|
||||
setBanner(`Live key browse failed on ${activeConnection.name} / ${activeDb}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "key_browse",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -634,13 +647,17 @@ function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const backendError = toBackendError(error);
|
||||
startTransition(() => {
|
||||
setSelectedValueRecord(null);
|
||||
setDraftValue("");
|
||||
setTtlDraft("");
|
||||
setIsInspectorLoading(false);
|
||||
setBanner(`Live value read failed for ${selectedKey.name} on ${activeConnection.name}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "value_read",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
keyName: selectedKey.name,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -665,7 +682,9 @@ function App() {
|
||||
setActiveConnectionId(connection.id);
|
||||
setActiveDb(formatDatabaseLabel(connection.target.database));
|
||||
setSelectedKeyId(null);
|
||||
setBanner(`Switched workspace context to ${connection.name} on ${formatDatabaseLabel(connection.target.database)}.`);
|
||||
showNeutralBanner(
|
||||
`Switched workspace context to ${connection.name} on ${formatDatabaseLabel(connection.target.database)}.`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -688,13 +707,13 @@ function App() {
|
||||
: connection,
|
||||
),
|
||||
);
|
||||
setBanner(`Scoped the workspace to ${database} for ${activeConnection.name}.`);
|
||||
showNeutralBanner(`Scoped the workspace to ${database} for ${activeConnection.name}.`);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateDraftConnection = () => {
|
||||
if (draftErrorList.length > 0) {
|
||||
setBanner(`Connection draft is incomplete: ${draftErrorList[0]}`);
|
||||
showNeutralBanner(`Connection draft is incomplete: ${draftErrorList[0]}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -721,19 +740,23 @@ function App() {
|
||||
setActiveConnectionId(nextConnection.id);
|
||||
setActiveDb(formatDatabaseLabel(nextDatabase));
|
||||
setSelectedKeyId(null);
|
||||
setBanner("A draft connection profile was added to the shell. Credentials remain memory-only.");
|
||||
showAccentBanner(
|
||||
"A draft connection profile was added to the shell. Credentials remain memory-only.",
|
||||
);
|
||||
setConnectionDialogOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveString = async () => {
|
||||
if (!selectedKey) {
|
||||
setBanner(`Select a key in ${activeDb} before attempting a shell-level save.`);
|
||||
showNeutralBanner(`Select a key in ${activeDb} before attempting a shell-level save.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tauriAvailable) {
|
||||
setBanner(`Saved mock string value for ${selectedKey.name} in ${activeDb}. Backend persistence is not wired yet.`);
|
||||
showNeutralBanner(
|
||||
`Saved mock string value for ${selectedKey.name} in ${activeDb}. Backend persistence is not wired yet.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -763,13 +786,19 @@ function App() {
|
||||
: formatRedisValueData(result.data),
|
||||
);
|
||||
setIsSaveRunning(false);
|
||||
setBanner(`Saved live string value for ${selectedKey.name} in ${activeDb} and preserved the current TTL boundary.`);
|
||||
showAccentBanner(
|
||||
`Saved live string value for ${selectedKey.name} in ${activeDb} and preserved the current TTL boundary.`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
const backendError = toBackendError(error);
|
||||
startTransition(() => {
|
||||
setIsSaveRunning(false);
|
||||
setBanner(`Live string save failed for ${selectedKey.name}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "value_write",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
keyName: selectedKey.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -780,23 +809,27 @@ function App() {
|
||||
}
|
||||
|
||||
setDeleteDialogOpen(false);
|
||||
setBanner(`Delete confirmation completed for ${selectedKey.name} in ${activeDb}. Mutation stays mocked until a delete contract is approved.`);
|
||||
showNeutralBanner(
|
||||
`Delete confirmation completed for ${selectedKey.name} in ${activeDb}. Mutation stays mocked until a delete contract is approved.`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleApplyTtl = async () => {
|
||||
if (!selectedKey) {
|
||||
setBanner(`Select a key in ${activeDb} before attempting a TTL update.`);
|
||||
showNeutralBanner(`Select a key in ${activeDb} before attempting a TTL update.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const validationError = validateTtlDraft(ttlDraft);
|
||||
if (validationError) {
|
||||
setBanner(`TTL update is invalid: ${validationError}`);
|
||||
showNeutralBanner(`TTL update is invalid: ${validationError}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tauriAvailable) {
|
||||
setBanner(`Browser dev mode keeps TTL changes in demo fallback for ${selectedKey.name}. Use desktop runtime for a live TTL update.`);
|
||||
showNeutralBanner(
|
||||
`Browser dev mode keeps TTL changes in demo fallback for ${selectedKey.name}. Use desktop runtime for a live TTL update.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -824,25 +857,33 @@ function App() {
|
||||
: "",
|
||||
);
|
||||
setIsTtlUpdateRunning(false);
|
||||
setBanner(`Updated live TTL for ${selectedKey.name} to ${formatRedisKeyTtl(result.metadata.ttl)} in ${activeDb}.`);
|
||||
showAccentBanner(
|
||||
`Updated live TTL for ${selectedKey.name} to ${formatRedisKeyTtl(result.metadata.ttl)} in ${activeDb}.`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
const backendError = toBackendError(error);
|
||||
startTransition(() => {
|
||||
setIsTtlUpdateRunning(false);
|
||||
setBanner(`Live TTL update failed for ${selectedKey.name}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "ttl_update",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
keyName: selectedKey.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePersistTtl = async () => {
|
||||
if (!selectedKey) {
|
||||
setBanner(`Select a key in ${activeDb} before attempting to remove TTL.`);
|
||||
showNeutralBanner(`Select a key in ${activeDb} before attempting to remove TTL.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tauriAvailable) {
|
||||
setBanner(`Browser dev mode keeps TTL removal in demo fallback for ${selectedKey.name}. Use desktop runtime for a live TTL change.`);
|
||||
showNeutralBanner(
|
||||
`Browser dev mode keeps TTL removal in demo fallback for ${selectedKey.name}. Use desktop runtime for a live TTL change.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -865,25 +906,35 @@ function App() {
|
||||
syncUpdatedMetadata(result.metadata);
|
||||
setTtlDraft("");
|
||||
setIsTtlUpdateRunning(false);
|
||||
setBanner(`Removed TTL for ${selectedKey.name}; the key is now ${formatRedisKeyTtl(result.metadata.ttl)}.`);
|
||||
showAccentBanner(
|
||||
`Removed TTL for ${selectedKey.name}; the key is now ${formatRedisKeyTtl(result.metadata.ttl)}.`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
const backendError = toBackendError(error);
|
||||
startTransition(() => {
|
||||
setIsTtlUpdateRunning(false);
|
||||
setBanner(`Removing TTL failed for ${selectedKey.name}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "ttl_remove",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
keyName: selectedKey.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshInspector = async () => {
|
||||
if (!selectedKey) {
|
||||
setBanner(`No key is selected in ${activeDb}. Adjust or clear the current search pattern first.`);
|
||||
showNeutralBanner(
|
||||
`No key is selected in ${activeDb}. Adjust or clear the current search pattern first.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tauriAvailable) {
|
||||
setBanner(`Refreshed shell metadata for ${selectedKey.name} in ${activeDb}. Live value reload remains downstream of backend browse contracts.`);
|
||||
showNeutralBanner(
|
||||
`Refreshed shell metadata for ${selectedKey.name} in ${activeDb}. Live value reload remains downstream of backend browse contracts.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -912,13 +963,19 @@ function App() {
|
||||
);
|
||||
setTtlDraft(result.metadata.ttl.kind === "expires_in_millis" ? String(result.metadata.ttl.value) : "");
|
||||
setIsInspectorLoading(false);
|
||||
setBanner(`Refreshed live metadata and value surface for ${selectedKey.name} in ${activeDb}.`);
|
||||
showAccentBanner(
|
||||
`Refreshed live metadata and value surface for ${selectedKey.name} in ${activeDb}.`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
const backendError = toBackendError(error);
|
||||
startTransition(() => {
|
||||
setIsInspectorLoading(false);
|
||||
setBanner(`Live metadata refresh failed for ${selectedKey.name}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "inspector_refresh",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
keyName: selectedKey.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -926,17 +983,21 @@ function App() {
|
||||
const handleCopyQaSnapshot = async () => {
|
||||
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
||||
setCopyFeedback("unavailable");
|
||||
setBanner("Clipboard export is unavailable in this runtime. Copy the QA snapshot text manually from the side rail.");
|
||||
showNeutralBanner(
|
||||
"Clipboard export is unavailable in this runtime. Copy the QA snapshot text manually from the side rail.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(qaSnapshot);
|
||||
setCopyFeedback("copied");
|
||||
setBanner("Copied the current QA snapshot to the clipboard for smoke evidence.");
|
||||
showAccentBanner("Copied the current QA snapshot to the clipboard for smoke evidence.");
|
||||
} catch {
|
||||
setCopyFeedback("unavailable");
|
||||
setBanner("Clipboard export failed in this runtime. Copy the QA snapshot text manually from the side rail.");
|
||||
showNeutralBanner(
|
||||
"Clipboard export failed in this runtime. Copy the QA snapshot text manually from the side rail.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -981,10 +1042,13 @@ function App() {
|
||||
setIsBrowseLoading(false);
|
||||
});
|
||||
} catch (error) {
|
||||
const backendError = toBackendError(error);
|
||||
startTransition(() => {
|
||||
setIsBrowseLoading(false);
|
||||
setBanner(`Loading more keys failed on ${activeConnection.name} / ${activeDb}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "key_browse",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1006,7 +1070,9 @@ function App() {
|
||||
: item,
|
||||
),
|
||||
);
|
||||
setBanner(`Testing ${connection.name} against ${activeDb} using the current desktop contract.`);
|
||||
showNeutralBanner(
|
||||
`Testing ${connection.name} against ${activeDb} using the current desktop contract.`,
|
||||
);
|
||||
});
|
||||
|
||||
if (!tauriAvailable) {
|
||||
@@ -1022,7 +1088,9 @@ function App() {
|
||||
: item,
|
||||
),
|
||||
);
|
||||
setBanner(`Tauri runtime is unavailable in browser dev mode, so connection testing stayed in demo fallback for ${connection.name}.`);
|
||||
showNeutralBanner(
|
||||
`Tauri runtime is unavailable in browser dev mode, so connection testing stayed in demo fallback for ${connection.name}.`,
|
||||
);
|
||||
setIsConnectionTestRunning(false);
|
||||
});
|
||||
return;
|
||||
@@ -1051,12 +1119,12 @@ function App() {
|
||||
),
|
||||
);
|
||||
setActiveDb(formatDatabaseLabel(result.selected_database));
|
||||
setBanner(`Live Redis test passed for ${connection.name} on ${formatDatabaseLabel(result.selected_database)}. Round-trip status: ${result.round_trip_status}.`);
|
||||
showAccentBanner(
|
||||
`Live Redis test passed for ${connection.name} on ${formatDatabaseLabel(result.selected_database)}. Round-trip status: ${result.round_trip_status}.`,
|
||||
);
|
||||
setIsConnectionTestRunning(false);
|
||||
});
|
||||
} catch (error) {
|
||||
const backendError = toBackendError(error);
|
||||
|
||||
startTransition(() => {
|
||||
setConnections((current) =>
|
||||
current.map((item) =>
|
||||
@@ -1064,12 +1132,16 @@ function App() {
|
||||
? {
|
||||
...item,
|
||||
status: "error",
|
||||
detail: backendError.code,
|
||||
detail: formatBackendErrorCodeLabel(toBackendError(error).code),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
setBanner(`Live Redis test failed for ${connection.name}: ${formatBackendError(backendError)}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "connection_test",
|
||||
connectionName: connection.name,
|
||||
databaseLabel: activeDb,
|
||||
});
|
||||
setIsConnectionTestRunning(false);
|
||||
});
|
||||
}
|
||||
@@ -1102,7 +1174,9 @@ function App() {
|
||||
},
|
||||
...current.slice(0, 4),
|
||||
]);
|
||||
setBanner(`Blocked ${command.toUpperCase()} in ${activeDb}. Administrative commands remain outside the visible shell.`);
|
||||
showNeutralBanner(
|
||||
`Blocked ${command.toUpperCase()} in ${activeDb}. Administrative commands remain outside the visible shell.`,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1122,7 +1196,9 @@ function App() {
|
||||
},
|
||||
...current.slice(0, 4),
|
||||
]);
|
||||
setBanner(`Tauri runtime is unavailable in browser dev mode, so ${command.toUpperCase()} ran against the visible demo data only.`);
|
||||
showNeutralBanner(
|
||||
`Tauri runtime is unavailable in browser dev mode, so ${command.toUpperCase()} ran against the visible demo data only.`,
|
||||
);
|
||||
setConsoleInput(trimmed);
|
||||
setIsCommandRunning(false);
|
||||
});
|
||||
@@ -1148,24 +1224,34 @@ function App() {
|
||||
},
|
||||
...current.slice(0, 4),
|
||||
]);
|
||||
setBanner(`Live Redis command completed on ${activeConnection.name} / ${formatDatabaseLabel(result.database)}.`);
|
||||
showAccentBanner(
|
||||
`Live Redis command completed on ${activeConnection.name} / ${formatDatabaseLabel(result.database)}.`,
|
||||
);
|
||||
setConsoleInput(trimmed);
|
||||
setIsCommandRunning(false);
|
||||
});
|
||||
} catch (error) {
|
||||
const backendError = toBackendError(error);
|
||||
|
||||
startTransition(() => {
|
||||
setConsoleHistory((current) => [
|
||||
{
|
||||
command: trimmed,
|
||||
output: formatBackendError(backendError),
|
||||
output: toBackendErrorNotice(error, {
|
||||
operation: "command_execution",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
commandName: command,
|
||||
}).message,
|
||||
status: "error",
|
||||
source: "live",
|
||||
},
|
||||
...current.slice(0, 4),
|
||||
]);
|
||||
setBanner(`Live Redis command failed on ${activeConnection.name} / ${activeDb}: ${backendError.message}.`);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "command_execution",
|
||||
connectionName: activeConnection.name,
|
||||
databaseLabel: activeDb,
|
||||
commandName: command,
|
||||
});
|
||||
setConsoleInput(trimmed);
|
||||
setIsCommandRunning(false);
|
||||
});
|
||||
@@ -1219,8 +1305,28 @@ function App() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mb-4 rounded-[24px] border border-[var(--line-soft)] bg-[var(--surface-raised)] px-4 py-3 shadow-[var(--shadow-panel)]">
|
||||
<p className="text-sm text-[var(--ink-soft)]">{banner}</p>
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 rounded-[24px] border px-4 py-3 shadow-[var(--shadow-panel)]",
|
||||
banner.tone === "danger"
|
||||
? "border-[var(--danger-soft)] bg-[var(--danger-faint)]"
|
||||
: banner.tone === "accent"
|
||||
? "border-[var(--accent-strong)] bg-[var(--accent-faint)]"
|
||||
: "border-[var(--line-soft)] bg-[var(--surface-raised)]",
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm",
|
||||
banner.tone === "danger"
|
||||
? "text-[var(--danger-strong)]"
|
||||
: banner.tone === "accent"
|
||||
? "text-[var(--accent-strong)]"
|
||||
: "text-[var(--ink-soft)]",
|
||||
)}
|
||||
>
|
||||
{banner.message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<main className="grid min-h-0 flex-1 gap-4 xl:grid-cols-[310px_minmax(360px,1.1fr)_minmax(420px,1fr)]">
|
||||
@@ -1902,6 +2008,10 @@ function formatConnectionCardDetail(connection: ConnectionProfile) {
|
||||
return connection.detail;
|
||||
}
|
||||
|
||||
function formatBackendErrorCodeLabel(code: string) {
|
||||
return code.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
function toRedisConnectionRequest(
|
||||
connection: ConnectionProfile,
|
||||
activeDb: string,
|
||||
@@ -1984,37 +2094,6 @@ function validateConnectionDraft(
|
||||
return errors;
|
||||
}
|
||||
|
||||
function toBackendError(error: unknown): BackendError {
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"message" in error &&
|
||||
typeof error.message === "string"
|
||||
) {
|
||||
return {
|
||||
code:
|
||||
"code" in error && typeof error.code === "string"
|
||||
? error.code
|
||||
: "internal",
|
||||
message: error.message,
|
||||
detail:
|
||||
"detail" in error && typeof error.detail === "string"
|
||||
? error.detail
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: "internal",
|
||||
message: error instanceof Error ? error.message : "Unknown backend error.",
|
||||
detail: null,
|
||||
};
|
||||
}
|
||||
|
||||
function formatBackendError(error: BackendError) {
|
||||
return error.detail ? `${error.message} (${error.detail})` : error.message;
|
||||
}
|
||||
|
||||
function formatRedisResponse(response: RedisResponse) {
|
||||
const normalized = normalizeRedisResponse(response);
|
||||
return typeof normalized === "string"
|
||||
|
||||
Reference in New Issue
Block a user