Persistent nodedb (#780)
* Refactor and consolitdate store imports - Created a new index file in the core stores directory to export all stores from a single module. - Updated imports to use consolidated store exports. * Remove unnecessary import * Update imports * First steps to persist nodeDB * Use named exports * Change store import after merge * Persistent nodeDB initial work * Key mishmatch warning, new serialization handler * Minor copilot changes * Add NODEDB_RETENTION_NUM * Updated tests * Refactor PKI mismatch logic * Clear persisted db on reset * Only persist on featureFlag * Mock featureFlag in tests --------- Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
co-authored by
philon-
parent
e33463b371
commit
68ec7ee5d8
@@ -10,7 +10,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { useDevice } from "@core/stores";
|
||||
import { useDevice, useNodeDB } from "@core/stores";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -33,7 +33,8 @@ export const DeviceNameDialog = ({
|
||||
onOpenChange,
|
||||
}: DeviceNameDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { hardware, getNode, connection } = useDevice();
|
||||
const { hardware, connection } = useDevice();
|
||||
const { getNode } = useNodeDB();
|
||||
const myNode = getNode(hardware.myNodeNum);
|
||||
|
||||
const defaultValues = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDevice } from "@core/stores";
|
||||
import { useNodeDB } from "@core/stores";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -23,7 +23,7 @@ export const LocationResponseDialog = ({
|
||||
onOpenChange,
|
||||
}: LocationResponseDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { getNode } = useDevice();
|
||||
const { getNode } = useNodeDB();
|
||||
|
||||
const from = getNode(location?.from ?? 0);
|
||||
const longName =
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
|
||||
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
|
||||
import { toast } from "@core/hooks/useToast.ts";
|
||||
import { useAppStore, useDevice } from "@core/stores";
|
||||
import { useAppStore, useDevice, useNodeDB } from "@core/stores";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
@@ -55,7 +55,8 @@ export const NodeDetailsDialog = ({
|
||||
onOpenChange,
|
||||
}: NodeDetailsDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { setDialogOpen, connection, getNode } = useDevice();
|
||||
const { setDialogOpen, connection } = useDevice();
|
||||
const { getNode } = useNodeDB();
|
||||
const navigate = useNavigate();
|
||||
const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore();
|
||||
const { updateFavorite } = useFavoriteNode();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -7,12 +8,11 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { useDevice, useNodeDB } from "@core/stores";
|
||||
import { fromByteArray } from "base64-js";
|
||||
import { DownloadIcon, PrinterIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDevice } from "../../core/stores";
|
||||
import { Button } from "../UI/Button.tsx";
|
||||
|
||||
export interface PkiBackupDialogProps {
|
||||
open: boolean;
|
||||
@@ -24,7 +24,8 @@ export const PkiBackupDialog = ({
|
||||
onOpenChange,
|
||||
}: PkiBackupDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { config, setDialogOpen, getMyNode } = useDevice();
|
||||
const { config, setDialogOpen } = useDevice();
|
||||
const { getMyNode } = useNodeDB();
|
||||
const privateKey = config.security?.privateKey;
|
||||
const publicKey = config.security?.publicKey;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, act } from "@testing-library/react";
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
ClassAttributes,
|
||||
@@ -89,7 +89,9 @@ describe("RebootDialog", () => {
|
||||
render(<RebootDialog open onOpenChange={() => {}} />);
|
||||
|
||||
// Schedule non-OTA reboot
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
});
|
||||
expect(rebootMock).toHaveBeenCalledWith(5);
|
||||
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -97,7 +99,9 @@ describe("RebootDialog", () => {
|
||||
rebootOtaMock.mockClear();
|
||||
|
||||
// Cancel scheduled
|
||||
fireEvent.click(screen.getByTestId("cancelRebootBtn"));
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId("cancelRebootBtn"));
|
||||
});
|
||||
expect(rebootMock).toHaveBeenCalledWith(-1);
|
||||
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -105,8 +109,12 @@ describe("RebootDialog", () => {
|
||||
rebootOtaMock.mockClear();
|
||||
|
||||
// Schedule OTA reboot
|
||||
fireEvent.click(screen.getByText(/reboot into ota mode/i));
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText(/reboot into ota mode/i));
|
||||
});
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
});
|
||||
expect(rebootOtaMock).toHaveBeenCalledWith(5);
|
||||
expect(rebootMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -115,17 +123,23 @@ describe("RebootDialog", () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||
target: { value: "3" },
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||
target: { value: "3" },
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
});
|
||||
|
||||
expect(rebootMock).toHaveBeenCalledWith(3);
|
||||
|
||||
expect(screen.getByText(/reboot has been scheduled/i)).toBeInTheDocument();
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(3000);
|
||||
});
|
||||
|
||||
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
||||
|
||||
@@ -135,7 +149,9 @@ describe("RebootDialog", () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /reboot now/i }));
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole("button", { name: /reboot now/i }));
|
||||
});
|
||||
|
||||
expect(rebootMock).toHaveBeenCalledWith(0);
|
||||
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
||||
@@ -148,9 +164,13 @@ describe("RebootDialog", () => {
|
||||
|
||||
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
expect(rebootMock).not.toHaveBeenCalled();
|
||||
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||
@@ -162,13 +182,19 @@ describe("RebootDialog", () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||
target: { value: "4" },
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||
target: { value: "4" },
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||
expect(rebootMock).toHaveBeenCalledWith(4);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||
});
|
||||
expect(rebootMock).toHaveBeenCalledWith(-1);
|
||||
expect(screen.queryByText(/reboot has been scheduled/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
|
||||
import { CurrentDeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
|
||||
import { render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
||||
@@ -37,11 +37,6 @@ test("does not render dialog if no error exists for active chat", () => {
|
||||
|
||||
useDeviceStore.getState().addDevice(deviceId);
|
||||
|
||||
const currentDeviceState = useDeviceStore.getState().getDevice(deviceId);
|
||||
if (!currentDeviceState) {
|
||||
throw new Error("Device not found");
|
||||
}
|
||||
|
||||
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
|
||||
mockUseRefreshKeysDialog.mockReturnValue({
|
||||
handleCloseDialog: vi.fn(),
|
||||
@@ -49,9 +44,9 @@ test("does not render dialog if no error exists for active chat", () => {
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<DeviceContext.Provider value={currentDeviceState}>
|
||||
<CurrentDeviceContext.Provider value={{ deviceId }}>
|
||||
<RefreshKeysDialog open onOpenChange={vi.fn()} />
|
||||
</DeviceContext.Provider>,
|
||||
</CurrentDeviceContext.Provider>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { useDevice, useMessageStore } from "@core/stores";
|
||||
import { useMessageStore, useNodeDB } from "@core/stores";
|
||||
import { LockKeyholeOpenIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
@@ -22,7 +22,8 @@ export const RefreshKeysDialog = ({
|
||||
}: RefreshKeysDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { activeChat } = useMessageStore();
|
||||
const { nodeErrors, getNode } = useDevice();
|
||||
const { nodeErrors, getNode } = useNodeDB();
|
||||
|
||||
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
|
||||
|
||||
const nodeErrorNum = nodeErrors.get(activeChat);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useDevice, useMessageStore } from "@core/stores";
|
||||
import { useDevice, useMessageStore, useNodeDB } from "@core/stores";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function useRefreshKeysDialog() {
|
||||
const { removeNode, setDialogOpen, clearNodeError, getNodeError } =
|
||||
useDevice();
|
||||
const { setDialogOpen } = useDevice();
|
||||
const { removeNode, clearNodeError, getNodeError } = useNodeDB();
|
||||
const { activeChat } = useMessageStore();
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { useAppStore, useDevice } from "@core/stores";
|
||||
import { useAppStore, useDevice, useNodeDB } from "@core/stores";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface RemoveNodeDialogProps {
|
||||
@@ -22,7 +22,8 @@ export const RemoveNodeDialog = ({
|
||||
onOpenChange,
|
||||
}: RemoveNodeDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection, getNode, removeNode } = useDevice();
|
||||
const { connection } = useDevice();
|
||||
const { getNode, removeNode } = useNodeDB();
|
||||
const { nodeNumToBeRemoved } = useAppStore();
|
||||
|
||||
const onSubmit = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDevice } from "@core/stores";
|
||||
import { useNodeDB } from "@core/stores";
|
||||
import type { Protobuf, Types } from "@meshtastic/core";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,7 +25,7 @@ export const TracerouteResponseDialog = ({
|
||||
onOpenChange,
|
||||
}: TracerouteResponseDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { getNode } = useDevice();
|
||||
const { getNode } = useNodeDB();
|
||||
const route: number[] = traceroute?.data.route ?? [];
|
||||
const routeBack: number[] = traceroute?.data.routeBack ?? [];
|
||||
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
|
||||
|
||||
@@ -36,7 +36,10 @@ const mockDevice = {
|
||||
setDialogOpen: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("@core/stores/deviceStore", () => ({
|
||||
vi.mock("@core/stores", () => ({
|
||||
CurrentDeviceContext: {
|
||||
_currentValue: { deviceId: 123 },
|
||||
},
|
||||
useDevice: () => ({
|
||||
setDialogOpen: mockDevice.setDialogOpen,
|
||||
}),
|
||||
@@ -85,7 +88,7 @@ describe("useUnsafeRolesDialog", () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[0],
|
||||
UNSAFE_ROLES[0]!,
|
||||
);
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
@@ -97,7 +100,7 @@ describe("useUnsafeRolesDialog", () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
onHandler({ action: "confirm" });
|
||||
const validationResult = await validationPromise;
|
||||
|
||||
@@ -111,9 +114,9 @@ describe("useUnsafeRolesDialog", () => {
|
||||
it("should resolve with false when user dismisses the dialog", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[0],
|
||||
UNSAFE_ROLES[0]!,
|
||||
);
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
onHandler({ action: "dismiss" });
|
||||
|
||||
const validationResult = await validationPromise;
|
||||
@@ -128,9 +131,9 @@ describe("useUnsafeRolesDialog", () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[1],
|
||||
UNSAFE_ROLES[1]!,
|
||||
);
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
|
||||
onHandler({ action: "confirm" });
|
||||
await validationPromise;
|
||||
@@ -157,7 +160,7 @@ describe("useUnsafeRolesDialog", () => {
|
||||
true,
|
||||
);
|
||||
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
onHandler({ action: "confirm" });
|
||||
|
||||
const validationResult = await validationPromise;
|
||||
|
||||
Reference in New Issue
Block a user