diff --git a/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.test.tsx b/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.test.tsx
index 94d31a93..41d82570 100644
--- a/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.test.tsx
+++ b/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.test.tsx
@@ -4,17 +4,12 @@ import { FactoryResetDeviceDialog } from "./FactoryResetDeviceDialog.tsx";
const mockFactoryResetDevice = vi.fn();
const mockRemoveDevice = vi.fn();
-const mockRemoveMessageStore = vi.fn();
const mockToast = vi.fn();
vi.mock("@core/stores", () => {
- // Make each store a callable fn (like a Zustand hook), and attach .getState()
const useDeviceStore = Object.assign(vi.fn(), {
getState: () => ({ removeDevice: mockRemoveDevice }),
});
- const useMessageStore = Object.assign(vi.fn(), {
- getState: () => ({ removeMessageStore: mockRemoveMessageStore }),
- });
return {
CurrentDeviceContext: {
@@ -25,7 +20,6 @@ vi.mock("@core/stores", () => {
connection: { factoryResetDevice: mockFactoryResetDevice },
}),
useDeviceStore,
- useMessageStore,
};
});
@@ -40,12 +34,10 @@ describe("FactoryResetDeviceDialog", () => {
mockOnOpenChange.mockClear();
mockFactoryResetDevice.mockReset();
mockRemoveDevice.mockClear();
- mockRemoveMessageStore.mockClear();
mockToast.mockClear();
});
- it("calls factoryResetDevice, closes dialog, and after reset resolves clears messages and node DB", async () => {
- // Control the promise returned by factoryResetDevice
+ it("calls factoryResetDevice, closes dialog, and after reset resolves removes the device", async () => {
let resolveReset: (() => void) | undefined;
mockFactoryResetDevice.mockImplementation(
() =>
@@ -57,20 +49,16 @@ describe("FactoryResetDeviceDialog", () => {
render();
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Device" }));
- // Called immediately
expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1);
- // DialogWrapper awaits onConfirm (which returns undefined), so close happens on next microtask
await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
- // Resolve the reset
resolveReset?.();
expect(mockRemoveDevice).toHaveBeenCalledTimes(1);
- expect(mockRemoveMessageStore).toHaveBeenCalledTimes(1);
});
it("calls onOpenChange(false) and does not call factoryResetDevice when cancel is clicked", async () => {
@@ -84,6 +72,5 @@ describe("FactoryResetDeviceDialog", () => {
expect(mockFactoryResetDevice).not.toHaveBeenCalled();
expect(mockRemoveDevice).not.toHaveBeenCalled();
- expect(mockRemoveMessageStore).not.toHaveBeenCalled();
});
});
diff --git a/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.tsx b/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.tsx
index afd32e14..42c90fca 100644
--- a/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.tsx
+++ b/packages/web/src/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog.tsx
@@ -1,5 +1,5 @@
import { toast } from "@core/hooks/useToast.ts";
-import { useDevice, useDeviceStore, useMessageStore } from "@core/stores";
+import { useDevice, useDeviceStore } from "@core/stores";
import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx";
@@ -23,7 +23,6 @@ export const FactoryResetDeviceDialog = ({ open, onOpenChange }: FactoryResetDev
// The device will be wiped and disconnected without resolving the promise
// so we proceed to clear all data associated with the device immediately
useDeviceStore.getState().removeDevice(id);
- useMessageStore.getState().removeMessageStore(id);
// Reload the app to ensure all ephemeral state is cleared
window.location.href = "/";
diff --git a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx b/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx
index fbfdec39..2098e04d 100644
--- a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx
+++ b/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx
@@ -1,4 +1,4 @@
-import { CurrentDeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
+import { CurrentDeviceContext, useDeviceStore } from "@core/stores";
import { MeshRegistry } from "@meshtastic/sdk";
import { MeshRegistryProvider } from "@meshtastic/sdk-react";
import { render } from "@testing-library/react";
@@ -6,16 +6,8 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
-vi.mock("@core/stores", async () => {
- const actual = (await vi.importActual("@core/stores")) as typeof import("@core/stores");
- return {
- ...actual,
- useMessageStore: vi.fn(),
- };
-});
vi.mock("./useRefreshKeysDialog");
-const mockUseMessageStore = vi.mocked(useMessageStore);
const mockUseRefreshKeysDialog = vi.mocked(useRefreshKeysDialog);
const getInitialState = () =>
@@ -33,20 +25,17 @@ afterEach(() => {
vi.restoreAllMocks();
});
-test("does not render dialog if no error exists for active chat", () => {
+test("does not render dialog when there are no node errors", () => {
const deviceId = 1;
- const activeChatNum = 54321;
useDeviceStore.getState().addDevice(deviceId);
- mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
mockUseRefreshKeysDialog.mockReturnValue({
handleCloseDialog: vi.fn(),
handleNodeRemove: vi.fn(),
});
- // Empty MeshRegistry so the SDK adapter hooks (useNodeAsProto) do not
- // throw when looking up the missing-key node — they return undefined.
+ // Empty MeshRegistry so useNodeErrors returns the empty fallback.
const registry = new MeshRegistry();
const { container } = render(
diff --git a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx b/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx
index 4da22a28..27cb90b9 100644
--- a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx
+++ b/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx
@@ -7,8 +7,7 @@ import {
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
-import { useMessages } from "@core/stores";
-import { useNodeError } from "@meshtastic/sdk-react";
+import { useNodeErrors } from "@meshtastic/sdk-react";
import { LockKeyholeOpenIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
@@ -20,8 +19,7 @@ export interface RefreshKeysDialogProps {
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
const { t } = useTranslation("dialog");
- const { activeChat } = useMessages();
- const nodeError = useNodeError(activeChat);
+ const nodeError = useNodeErrors()[0];
const nodeWithError = useNodeAsProto(nodeError?.node ?? 0);
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
diff --git a/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts b/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts
index 3694f0b9..d1b1046d 100644
--- a/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts
+++ b/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts
@@ -1,24 +1,22 @@
-import { useDevice, useMessages } from "@core/stores";
-import { useActiveClient } from "@meshtastic/sdk-react";
+import { useDevice } from "@core/stores";
+import { useActiveClient, useNodeErrors } from "@meshtastic/sdk-react";
import { useCallback } from "react";
export function useRefreshKeysDialog() {
const { setDialogOpen } = useDevice();
const meshClient = useActiveClient();
- const { activeChat } = useMessages();
+ const error = useNodeErrors()[0];
const handleCloseDialog = useCallback(() => {
setDialogOpen("refreshKeys", false);
}, [setDialogOpen]);
const handleNodeRemove = useCallback(() => {
- if (!meshClient) return;
- const error = meshClient.nodes.errorFor(activeChat);
- if (!error) return;
- meshClient.nodes.clearError(activeChat);
+ if (!meshClient || !error) return;
+ meshClient.nodes.clearError(error.node);
handleCloseDialog();
void meshClient.nodes.remove(error.node);
- }, [meshClient, activeChat, handleCloseDialog]);
+ }, [meshClient, error, handleCloseDialog]);
return {
handleCloseDialog,
diff --git a/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx b/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx
index 5335b00b..67d17a1f 100644
--- a/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx
+++ b/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx
@@ -1,7 +1,7 @@
import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx";
import { Separator } from "@components/UI/Separator";
import { Skeleton } from "@components/UI/Skeleton.tsx";
-import type { Message } from "@core/stores/messageStore/types.ts";
+import type { Message } from "@core/stores/messageStore";
import type { TFunction } from "i18next";
import { InboxIcon } from "lucide-react";
import { Fragment, Suspense, useMemo } from "react";
diff --git a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx
index e03b4c42..6b975bc9 100644
--- a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx
+++ b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx
@@ -7,8 +7,8 @@ import {
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import { useMyNodeAsProto, useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
-import { MessageState, useAppStore, useDevice } from "@core/stores";
-import type { Message } from "@core/stores/messageStore/types.ts";
+import { useAppStore, useDevice } from "@core/stores";
+import { type Message, MessageState } from "@core/stores/messageStore";
import { cn } from "@core/utils/cn.ts";
import { type Protobuf, Types } from "@meshtastic/sdk";
import type { LucideIcon } from "lucide-react";
diff --git a/packages/web/src/core/hooks/useChatLegacy.ts b/packages/web/src/core/hooks/useChatLegacy.ts
index 19accc28..8f31d177 100644
--- a/packages/web/src/core/hooks/useChatLegacy.ts
+++ b/packages/web/src/core/hooks/useChatLegacy.ts
@@ -1,5 +1,8 @@
-import { MessageState as LegacyMessageState, MessageType } from "@core/stores";
-import type { Message as LegacyMessage } from "@core/stores/messageStore/types.ts";
+import {
+ type Message as LegacyMessage,
+ MessageState as LegacyMessageState,
+ MessageType,
+} from "@core/stores/messageStore";
import type { Message as SdkMessage } from "@meshtastic/sdk";
import { MessageState as SdkMessageState, type Types } from "@meshtastic/sdk";
import { useChat, useDirectChat } from "@meshtastic/sdk-react";
diff --git a/packages/web/src/core/hooks/useNewNodeNum.ts b/packages/web/src/core/hooks/useNewNodeNum.ts
index 41b62192..5e9e1f15 100644
--- a/packages/web/src/core/hooks/useNewNodeNum.ts
+++ b/packages/web/src/core/hooks/useNewNodeNum.ts
@@ -1,7 +1,6 @@
-import { useDeviceStore, useMessageStore } from "@core/stores";
+import { useDeviceStore } from "@core/stores";
import type { Protobuf } from "@meshtastic/sdk";
export function useNewNodeNum(id: number, nodeInfo: Protobuf.Mesh.MyNodeInfo): void {
useDeviceStore.getState().getDevice(id)?.setHardware(nodeInfo);
- useMessageStore.getState().getMessageStore(id)?.setNodeNum(nodeInfo.myNodeNum);
}
diff --git a/packages/web/src/core/stores/index.ts b/packages/web/src/core/stores/index.ts
index a4d6f601..372bda83 100644
--- a/packages/web/src/core/stores/index.ts
+++ b/packages/web/src/core/stores/index.ts
@@ -1,6 +1,5 @@
import { useDeviceContext } from "@core/hooks/useDeviceContext.ts";
import { type Device, useDeviceStore } from "@core/stores/deviceStore/index.ts";
-import { type MessageStore, useMessageStore } from "@core/stores/messageStore/index.ts";
export {
CurrentDeviceContext,
@@ -31,12 +30,7 @@ export type {
ValidModuleConfigType,
WaypointWithMetadata,
} from "@core/stores/deviceStore/types.ts";
-export {
- MessageState,
- type MessageStore,
- MessageType,
- useMessageStore,
-} from "@core/stores/messageStore";
+export { MessageState, MessageType } from "@core/stores/messageStore";
export {
SidebarProvider,
useSidebar, // TODO: Bring hook into this file
@@ -51,10 +45,3 @@ export const useDevice = (): Device => {
const device = useDeviceStore((s) => s.getDevice(deviceId) ?? s.addDevice(deviceId));
return device;
};
-
-export const useMessages = (): MessageStore => {
- const { deviceId } = useDeviceContext();
-
- const device = useMessageStore((s) => s.getMessageStore(deviceId) ?? s.addMessageStore(deviceId));
- return device;
-};
diff --git a/packages/web/src/core/stores/messageStore/index.ts b/packages/web/src/core/stores/messageStore/index.ts
index 5e8f0ca4..55eaf993 100644
--- a/packages/web/src/core/stores/messageStore/index.ts
+++ b/packages/web/src/core/stores/messageStore/index.ts
@@ -1,26 +1,11 @@
-import { featureFlags } from "@core/services/featureFlags";
-import type {
- ChannelId,
- ClearMessageParams,
- ConversationId,
- GetMessagesParams,
- Message,
- MessageId,
- MessageLogMap,
- NodeNum,
- SetMessageStateParams,
-} from "@core/stores/messageStore/types.ts";
-import { evictOldestEntries } from "@core/stores/utils/evictOldestEntries.ts";
-import { createStorage } from "@core/stores/utils/indexDB.ts";
-import type { Types } from "@meshtastic/sdk";
-import { produce } from "immer";
-import { create as createStore, type StateCreator } from "zustand";
-import { type PersistOptions, persist } from "zustand/middleware";
+/**
+ * Legacy enums and message-shape types preserved for components that still
+ * read from the SDK chat slice via the `useChatLegacy` adapter. The Zustand
+ * messageStore that previously lived here has been retired — chat
+ * persistence is owned by the SDK ChatClient + SqlocalMessageRepository.
+ */
-const IDB_KEY_NAME = "meshtastic-message-store";
-const CURRENT_STORE_VERSION = 0;
-const MESSAGESTORE_RETENTION_NUM = 10;
-const MESSAGELOG_RETENTION_NUM = 1000; // Max messages per conversation/channel
+import type { Types } from "@meshtastic/sdk";
export enum MessageState {
Ack = "ack",
@@ -33,395 +18,27 @@ export enum MessageType {
Broadcast = "broadcast",
}
+export type NodeNum = number;
+export type MessageId = number;
+export type ChannelId = Types.ChannelNumber;
+export type ConversationId = string;
+
+interface MessageBase {
+ channel: Types.ChannelNumber;
+ to: number;
+ from: number;
+ date: number;
+ messageId: number;
+ state: MessageState;
+ message: string;
+}
+
+interface GenericMessage extends MessageBase {
+ type: T;
+}
+
+export type Message = GenericMessage | GenericMessage;
+
export function getConversationId(node1: NodeNum, node2: NodeNum): ConversationId {
return [node1, node2].sort((a, b) => a - b).join(":");
}
-
-export interface MessageBuckets {
- direct: Map;
- broadcast: Map;
-}
-
-type MessageStoreData = {
- // Persisted data
- id: number;
- myNodeNum: number | undefined;
- messages: MessageBuckets;
- drafts: Map;
-};
-
-export interface MessageStore extends MessageStoreData {
- // Ephemeral state (not persisted)
- activeChat: number;
- chatType: MessageType;
-
- setNodeNum: (nodeNum: number) => void;
- saveMessage: (message: Message) => void;
- setMessageState: (params: SetMessageStateParams) => void;
- getMessages: (params: GetMessagesParams) => Message[];
-
- getDraft: (key: Types.Destination) => string;
- setDraft: (key: Types.Destination, message: string) => void;
- clearDraft: (key: Types.Destination) => void;
- //clearAllDrafts: (key: Types.Destination) => void;
-
- deleteAllMessages: () => void;
- clearMessageByMessageId: (params: ClearMessageParams) => void;
-}
-
-export interface MessageStoreState {
- addMessageStore: (id: number) => MessageStore;
- removeMessageStore: (id: number) => void;
- getMessageStore: (id: number) => MessageStore | undefined;
- getMessageStores: () => MessageStore[];
-}
-interface PrivateMessageStoreState extends MessageStoreState {
- messageStores: Map;
-}
-
-type MessageStorePersisted = {
- messageStores: Map;
-};
-
-function messageStoreFactory(
- id: number,
- get: () => PrivateMessageStoreState,
- set: typeof useMessageStore.setState,
- data?: Partial,
-): MessageStore {
- const messages = data?.messages ?? {
- direct: new Map(),
- broadcast: new Map(),
- };
- const drafts = data?.drafts ?? new Map();
- const myNodeNum = data?.myNodeNum;
- const activeChat = 0;
- const chatType = MessageType.Broadcast;
-
- return {
- id,
- myNodeNum,
- messages,
- drafts,
- activeChat,
- chatType,
-
- setNodeNum: (nodeNum) => {
- set(
- produce((draft) => {
- const newStore = draft.messageStores.get(id);
- if (!newStore) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- newStore.myNodeNum = nodeNum;
-
- for (const [otherId, oldStore] of draft.messageStores) {
- if (otherId === id || oldStore.myNodeNum !== nodeNum) {
- continue;
- }
-
- // Adopt broadcast conversations (reuses inner Map references)
- for (const [channelId, logMap] of oldStore.messages.broadcast) {
- newStore.messages.broadcast.set(channelId, logMap);
- }
-
- // Adopt direct conversations
- for (const [conversationId, logMap] of oldStore.messages.direct) {
- newStore.messages.direct.set(conversationId, logMap);
- }
-
- // Adopt drafts
- for (const [destination, draftText] of oldStore.drafts) {
- newStore.drafts.set(destination, draftText);
- }
-
- // Drop old store
- draft.messageStores.delete(otherId);
- }
- }),
- );
- },
-
- saveMessage: (message: Message) => {
- set(
- produce((draft) => {
- const state = draft.messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- let log: MessageLogMap | undefined;
- if (message.type === MessageType.Direct) {
- const conversationId = getConversationId(message.from, message.to);
- if (!state.messages.direct.has(conversationId)) {
- state.messages.direct.set(conversationId, new Map());
- }
-
- log = state.messages.direct.get(conversationId);
- log?.set(message.messageId, message);
- } else if (message.type === MessageType.Broadcast) {
- const channelId = message.channel as ChannelId;
- if (!state.messages.broadcast.has(channelId)) {
- state.messages.broadcast.set(channelId, new Map());
- }
-
- log = state.messages.broadcast.get(channelId);
- log?.set(message.messageId, message);
- }
-
- if (log) {
- // Enforce retention limit
- evictOldestEntries(log, MESSAGELOG_RETENTION_NUM);
- }
- }),
- );
- },
-
- setMessageState: (params: SetMessageStateParams) => {
- set(
- produce((draft) => {
- const state = draft.messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- let messageLog: MessageLogMap | undefined;
- let targetMessage: Message | undefined;
-
- if (params.type === MessageType.Direct) {
- const conversationId = getConversationId(params.nodeA, params.nodeB);
- messageLog = state.messages.direct.get(conversationId);
- if (messageLog) {
- targetMessage = messageLog.get(params.messageId);
- }
- } else {
- // Broadcast
- messageLog = state.messages.broadcast.get(params.channelId);
- if (messageLog) {
- targetMessage = messageLog.get(params.messageId);
- }
- }
-
- if (targetMessage) {
- targetMessage.state = params.newState ?? MessageState.Ack;
- } else {
- console.warn(
- `Message or conversation/channel not found for state update. Params: ${JSON.stringify(
- params,
- )}`,
- );
- }
- }),
- );
- },
-
- getMessages: (params: GetMessagesParams): Message[] => {
- const state = get().messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- let messageMap: MessageLogMap | undefined;
-
- if (params.type === MessageType.Direct) {
- const conversationId = getConversationId(params.nodeA, params.nodeB);
- messageMap = state.messages.direct.get(conversationId);
- } else {
- messageMap = state.messages.broadcast.get(params.channelId);
- }
-
- if (messageMap === undefined) {
- return [];
- }
-
- const messagesArray = Array.from(messageMap.values());
- messagesArray.sort((a, b) => a.date - b.date);
- return messagesArray;
- },
-
- getDraft: (key) => {
- const state = get().messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- return state.drafts.get(key) ?? "";
- },
- setDraft: (key, message) => {
- set(
- produce((draft) => {
- const state = draft.messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- state.drafts.set(key, message);
- }),
- );
- },
- clearDraft: (key) => {
- set(
- produce((draft) => {
- const state = draft.messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- state.drafts.delete(key);
- }),
- );
- },
-
- deleteAllMessages: () => {
- set(
- produce((draft) => {
- const state = draft.messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- state.messages.direct = new Map();
- state.messages.broadcast = new Map();
- }),
- );
- },
-
- clearMessageByMessageId: (params: ClearMessageParams) => {
- set(
- produce((draft) => {
- const state = draft.messageStores.get(id);
- if (!state) {
- throw new Error(`No MessageStore found for id: ${id}`);
- }
-
- let messageLog: MessageLogMap | undefined;
- let parentMap: Map;
- let parentKey: ConversationId | ChannelId;
-
- if (params.type === MessageType.Direct) {
- parentKey = getConversationId(params.nodeA, params.nodeB);
- parentMap = state.messages.direct;
- messageLog = parentMap.get(parentKey);
- } else {
- // Broadcast
- parentKey = params.channelId;
- parentMap = state.messages.broadcast;
- messageLog = parentMap.get(parentKey);
- }
-
- if (messageLog) {
- const deleted = messageLog.delete(params.messageId);
-
- if (deleted) {
- console.log(
- `Deleted message ${params.messageId} from ${params.type} message ${parentKey}`,
- );
- // Clean up empty MessageLogMap and its entry in the parent map
- if (messageLog.size === 0) {
- parentMap.delete(parentKey);
- console.log(`Cleaned up empty message entry for ${parentKey}`);
- }
- } else {
- console.warn(
- `Message ${params.messageId} not found in ${params.type} chat ${parentKey} for deletion.`,
- );
- }
- } else {
- console.warn(`Message entry ${parentKey} not found for message deletion.`);
- }
- }),
- );
- },
- };
-}
-
-export const messageStoreInitializer: StateCreator = (set, get) => ({
- messageStores: new Map(),
-
- addMessageStore: (id) => {
- const existing = get().messageStores.get(id);
- if (existing) {
- return existing;
- }
-
- const nodeStore = messageStoreFactory(id, get, set);
- set(
- produce((draft) => {
- draft.messageStores.set(id, nodeStore);
-
- // Enforce retention limit
- evictOldestEntries(draft.messageStores, MESSAGESTORE_RETENTION_NUM);
- }),
- );
-
- return nodeStore;
- },
- removeMessageStore: (id) => {
- set(
- produce((draft) => {
- draft.messageStores.delete(id);
- }),
- );
- },
- getMessageStores: () => Array.from(get().messageStores.values()),
- getMessageStore: (id) => get().messageStores.get(id),
-});
-
-const persistOptions: PersistOptions = {
- name: IDB_KEY_NAME,
- storage: createStorage(),
- version: CURRENT_STORE_VERSION,
- partialize: (s): MessageStorePersisted => ({
- messageStores: new Map(
- Array.from(s.messageStores.entries()).map(([id, db]) => [
- id,
- {
- id: db.id,
- myNodeNum: db.myNodeNum,
- messages: db.messages,
- drafts: db.drafts,
- },
- ]),
- ),
- }),
- onRehydrateStorage: () => (state) => {
- if (!state) {
- return;
- }
- console.debug(
- "MessageStoreStore: Rehydrating state with ",
- state.messageStores.size,
- " MessageStores -",
- state.messageStores,
- );
-
- useMessageStore.setState(
- produce((draft) => {
- const rebuilt = new Map();
- for (const [id, data] of (
- draft.messageStores as unknown as Map
- ).entries()) {
- if (data.myNodeNum !== undefined) {
- // Only rebuild if there is a nodenum set otherwise orphan dbs will acumulate
- rebuilt.set(
- id,
- messageStoreFactory(id, useMessageStore.getState, useMessageStore.setState, data),
- );
- }
- }
- draft.messageStores = rebuilt;
- }),
- );
- },
-};
-
-// Add persist middleware on the store if the feature flag is enabled
-const persistMessages = featureFlags.get("persistMessages");
-console.debug(`MessageStore: Persisting messages is ${persistMessages ? "enabled" : "disabled"}`);
-
-export const useMessageStore = persistMessages
- ? createStore(
- persist(messageStoreInitializer, persistOptions),
- )
- : createStore()(messageStoreInitializer);
diff --git a/packages/web/src/core/stores/messageStore/messageStore.test.ts b/packages/web/src/core/stores/messageStore/messageStore.test.ts
deleted file mode 100644
index fafe1f39..00000000
--- a/packages/web/src/core/stores/messageStore/messageStore.test.ts
+++ /dev/null
@@ -1,727 +0,0 @@
-/** biome-ignore-all lint/style/noNonNullAssertion: */
-import { Types } from "@meshtastic/sdk";
-import { setAutoFreeze } from "immer";
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { getConversationId, MessageState, MessageType } from "./index.ts";
-import type { ChannelId, Message } from "./types.ts";
-
-const idbMem = new Map();
-vi.mock("idb-keyval", () => ({
- get: vi.fn((key: string) => Promise.resolve(idbMem.get(key))),
- set: vi.fn((key: string, val: string) => {
- idbMem.set(key, val);
- return Promise.resolve();
- }),
- del: vi.fn((k: string) => {
- idbMem.delete(k);
- return Promise.resolve();
- }),
-}));
-
-async function freshStore(persist = false) {
- vi.resetModules();
-
- // suppress console output from the store during tests (for github actions)
- vi.spyOn(console, "debug").mockImplementation(() => {});
- vi.spyOn(console, "log").mockImplementation(() => {});
- vi.spyOn(console, "info").mockImplementation(() => {});
-
- // Mock feature flag for persistence
- vi.doMock("@core/services/featureFlags", () => ({
- featureFlags: {
- get: vi.fn((key: string) => (key === "persistMessages" ? persist : false)),
- },
- }));
-
- const mod = await import("./index.ts");
- return mod;
-}
-
-const myNodeNum = 111;
-const otherNodeNum1 = 222;
-const otherNodeNum2 = 333;
-const broadcastChannel: ChannelId = 0;
-
-const directMessageToOther1: Message = {
- type: MessageType.Direct,
- from: myNodeNum,
- to: otherNodeNum1,
- channel: 0,
- date: Date.now(),
- messageId: 101,
- state: MessageState.Waiting,
- message: "Hello other 1 from me",
-};
-
-const directMessageFromOther1: Message = {
- type: MessageType.Direct,
- from: otherNodeNum1,
- to: myNodeNum,
- channel: 0,
- date: Date.now() + 1000,
- messageId: 102,
- state: MessageState.Waiting,
- message: "Hello me from other 1",
-};
-
-const directMessageToOther2: Message = {
- type: MessageType.Direct,
- from: myNodeNum,
- to: otherNodeNum2,
- channel: 0,
- date: Date.now() + 2000,
- messageId: 103,
- state: MessageState.Waiting,
- message: "Hello other 2 from me",
-};
-
-const broadcastMessage1: Message = {
- type: MessageType.Broadcast,
- from: otherNodeNum1,
- to: 0xffffffff,
- channel: broadcastChannel,
- date: Date.now() + 3000,
- messageId: 201,
- state: MessageState.Waiting,
- message: "Broadcast message 1",
-};
-
-const broadcastMessage2: Message = {
- type: MessageType.Broadcast,
- from: myNodeNum,
- to: 0xffffffff,
- channel: broadcastChannel,
- date: Date.now() + 4000,
- messageId: 202,
- state: MessageState.Waiting,
- message: "Broadcast message 2",
-};
-
-describe("MessageStore persistence & rehydrate", () => {
- beforeEach(() => {
- idbMem.clear();
- vi.clearAllMocks();
- });
-
- it("should have correct initial state", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- const store = state.addMessageStore(123);
-
- expect(store.messages.direct).toBeInstanceOf(Map);
- expect(store.messages.direct.size).toBe(0);
- expect(store.messages.broadcast).toBeInstanceOf(Map);
- expect(store.messages.broadcast.size).toBe(0);
- expect(store.drafts).toBeInstanceOf(Map);
- expect(store.drafts.size).toBe(0);
- expect(store.myNodeNum).toBe(undefined);
- expect(store.activeChat).toBe(0);
- expect(store.chatType).toBe(MessageType.Broadcast);
- });
-
- it("should set nodeNum", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- const db = state.addMessageStore(123);
-
- db.setNodeNum(myNodeNum);
- expect(useMessageStore.getState().getMessageStore(123)?.myNodeNum).toBe(myNodeNum);
- });
-
- describe("saveMessage", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- state.addMessageStore(123);
-
- it("should save a direct message with correct Map structure", () => {
- state.getMessageStore(123)?.saveMessage(directMessageToOther1);
- const conversationId = getConversationId(
- directMessageToOther1.from,
- directMessageToOther1.to,
- );
-
- const store = state.getMessageStore(123)!;
-
- // Check if the conversation Map exists
- expect(store.messages.direct.has(conversationId)).toBe(true);
- const conversationLog = store.messages.direct.get(conversationId);
- // Check if the inner Map (MessageLogMap) exists and is a Map
- expect(conversationLog).toBeInstanceOf(Map);
- // Check if the message exists within the inner Map
- expect(conversationLog?.has(directMessageToOther1.messageId)).toBe(true);
- // Check the message content
- expect(conversationLog?.get(directMessageToOther1.messageId)).toEqual(directMessageToOther1);
- });
-
- it("should save a broadcast message with correct Map structure", () => {
- state.getMessageStore(123)?.saveMessage(broadcastMessage1);
- const store = state.getMessageStore(123)!;
- const channelId = broadcastMessage1.channel;
-
- expect(store.messages.broadcast.has(channelId)).toBe(true);
- const channelLog = store.messages.broadcast.get(channelId);
- expect(channelLog).toBeInstanceOf(Map);
- expect(channelLog?.has(broadcastMessage1.messageId)).toBe(true);
- expect(channelLog?.get(broadcastMessage1.messageId)).toEqual(broadcastMessage1);
- });
-
- it("should save multiple messages correctly", () => {
- state.getMessageStore(123)?.saveMessage(directMessageToOther1);
- state.getMessageStore(123)?.saveMessage(directMessageFromOther1);
- state.getMessageStore(123)?.saveMessage(broadcastMessage1);
-
- const store = state.getMessageStore(123)!;
-
- const convId1 = getConversationId(myNodeNum, otherNodeNum1);
- expect(store.messages.direct.get(convId1)?.get(directMessageToOther1.messageId)).toEqual(
- directMessageToOther1,
- );
-
- expect(store.messages.direct.get(convId1)?.get(directMessageFromOther1.messageId)).toEqual(
- directMessageFromOther1,
- );
-
- const channelId = broadcastMessage1.channel;
- expect(store.messages.broadcast.get(channelId)?.get(broadcastMessage1.messageId)).toEqual(
- broadcastMessage1,
- );
- });
- });
-
- describe("getMessages", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- state.addMessageStore(123);
-
- beforeEach(() => {
- state.getMessageStore(123)?.setNodeNum(myNodeNum);
- state.getMessageStore(123)?.saveMessage(directMessageToOther1);
- state.getMessageStore(123)?.saveMessage(directMessageFromOther1);
- state.getMessageStore(123)?.saveMessage(directMessageToOther2);
- state.getMessageStore(123)?.saveMessage(broadcastMessage1);
- state.getMessageStore(123)?.saveMessage(broadcastMessage2);
- });
-
- it("should return broadcast messages for a channel, sorted by date", () => {
- const messages = state.getMessageStore(123)!.getMessages({
- type: MessageType.Broadcast,
- channelId: broadcastChannel,
- });
- expect(messages).toHaveLength(2);
- expect(messages[0]).toEqual(broadcastMessage1);
- expect(messages[1]).toEqual(broadcastMessage2);
- });
-
- it("should return empty array for broadcast if channel has no messages", () => {
- const messages = state.getMessageStore(123)!.getMessages({
- type: MessageType.Broadcast,
- channelId: Types.ChannelNumber.Channel1,
- });
- expect(messages).toEqual([]);
- });
-
- it("should return combined direct messages for a specific chat pair, sorted by date", () => {
- const messages = state.getMessageStore(123)!.getMessages({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: otherNodeNum1,
- });
- expect(messages).toHaveLength(2);
- expect(messages[0]).toEqual(directMessageToOther1);
- expect(messages[1]).toEqual(directMessageFromOther1);
- });
-
- it("should return only relevant direct messages for a different chat pair", () => {
- const messages = state.getMessageStore(123)!.getMessages({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: otherNodeNum2,
- });
- expect(messages).toHaveLength(1);
- expect(messages[0]).toEqual(directMessageToOther2);
- });
-
- it("should return empty array for direct chat if no messages exist", () => {
- const messages = state.getMessageStore(123)!.getMessages({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: 999,
- });
- expect(messages).toEqual([]);
- });
- });
-
- describe("setMessageState", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- state.addMessageStore(123);
-
- beforeEach(() => {
- state.getMessageStore(123)?.setNodeNum(myNodeNum);
- state.getMessageStore(123)?.saveMessage(directMessageToOther1);
- state.getMessageStore(123)?.saveMessage(directMessageFromOther1);
- state.getMessageStore(123)?.saveMessage(broadcastMessage1);
- });
-
- it("should update state for a direct message", () => {
- state.getMessageStore(123)!.setMessageState({
- type: MessageType.Direct,
- nodeA: directMessageToOther1.from,
- nodeB: directMessageToOther1.to,
- messageId: directMessageToOther1.messageId,
- newState: MessageState.Ack,
- });
- const conversationId = getConversationId(
- directMessageToOther1.from,
- directMessageToOther1.to,
- );
- const message = useMessageStore
- .getState()
- .getMessageStore(123)!
- .messages.direct.get(conversationId)
- ?.get(directMessageToOther1.messageId);
- expect(message?.state).toBe(MessageState.Ack);
- });
-
- it("should update state for another direct message in the same conversation", () => {
- state.getMessageStore(123)!.setMessageState({
- type: MessageType.Direct,
- nodeA: directMessageFromOther1.from,
- nodeB: directMessageFromOther1.to,
- messageId: directMessageFromOther1.messageId,
- newState: MessageState.Failed,
- });
- const conversationId = getConversationId(
- directMessageFromOther1.from,
- directMessageFromOther1.to,
- );
- const message = useMessageStore
- .getState()
- .getMessageStore(123)!
- .messages.direct.get(conversationId)
- ?.get(directMessageFromOther1.messageId);
- expect(message?.state).toBe(MessageState.Failed);
- });
-
- it("should update state for a broadcast message", () => {
- state.getMessageStore(123)!.setMessageState({
- type: MessageType.Broadcast,
- channelId: broadcastChannel,
- messageId: broadcastMessage1.messageId,
- newState: MessageState.Ack,
- });
- const message = useMessageStore
- .getState()
- .getMessageStore(123)!
- .messages.broadcast.get(broadcastChannel)
- ?.get(broadcastMessage1.messageId);
- expect(message?.state).toBe(MessageState.Ack);
- });
-
- it("should warn if message is not found (direct)", () => {
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
- state.getMessageStore(123)!.setMessageState({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: otherNodeNum1,
- messageId: 999,
- newState: MessageState.Ack,
- });
- expect(warnSpy).toHaveBeenCalledWith(
- expect.stringContaining("Message or conversation/channel not found for state update"),
- );
- warnSpy.mockRestore();
- });
-
- it("should warn if message is not found (broadcast)", () => {
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
- state.getMessageStore(123)!.setMessageState({
- type: MessageType.Broadcast,
- channelId: broadcastChannel,
- messageId: 999,
- newState: MessageState.Ack,
- });
- expect(warnSpy).toHaveBeenCalledWith(
- expect.stringContaining("Message or conversation/channel not found for state update"),
- );
- warnSpy.mockRestore();
- });
-
- it("should warn if conversation/channel is not found", () => {
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
- state.getMessageStore(123)!.setMessageState({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: 998,
- messageId: 101,
- newState: MessageState.Ack,
- });
- expect(warnSpy).toHaveBeenCalledWith(
- expect.stringContaining("Message or conversation/channel not found for state update"),
- );
- warnSpy.mockRestore();
- });
- });
-
- describe("clearMessageByMessageId", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- state.addMessageStore(123);
-
- const extraDirectMessageId = 1011;
- beforeEach(() => {
- state.getMessageStore(123)?.setNodeNum(myNodeNum);
- state.getMessageStore(123)?.saveMessage(directMessageToOther1);
- state.getMessageStore(123)?.saveMessage(directMessageFromOther1);
- state.getMessageStore(123)?.saveMessage(broadcastMessage1);
- state.getMessageStore(123)?.saveMessage({
- ...directMessageToOther1,
- messageId: extraDirectMessageId,
- date: Date.now() + 50,
- });
- });
-
- it("should delete a specific direct message", () => {
- const messageIdToDelete = directMessageToOther1.messageId;
- const nodeA = directMessageToOther1.from;
- const nodeB = directMessageToOther1.to;
- const conversationId = getConversationId(nodeA, nodeB);
-
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Direct,
- nodeA: nodeA,
- nodeB: nodeB,
- messageId: messageIdToDelete,
- });
-
- const store = useMessageStore.getState().getMessageStore(123)!;
- const conversationLog = store.messages.direct.get(conversationId);
- expect(conversationLog?.has(messageIdToDelete)).toBe(false);
- expect(conversationLog?.has(extraDirectMessageId)).toBe(true);
- expect(conversationLog?.has(directMessageFromOther1.messageId)).toBe(true);
- expect(store.messages.direct.has(conversationId)).toBe(true);
- });
-
- it("should delete another specific direct message", () => {
- const messageIdToDelete = directMessageFromOther1.messageId;
- const nodeA = directMessageFromOther1.from;
- const nodeB = directMessageFromOther1.to;
- const conversationId = getConversationId(nodeA, nodeB);
-
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Direct,
- nodeA: nodeA,
- nodeB: nodeB,
- messageId: messageIdToDelete,
- });
-
- const store = useMessageStore.getState().getMessageStore(123)!;
- const conversationLog = store.messages.direct.get(conversationId);
- expect(conversationLog?.has(messageIdToDelete)).toBe(false);
- expect(conversationLog?.has(directMessageToOther1.messageId)).toBe(true);
- expect(conversationLog?.has(extraDirectMessageId)).toBe(true);
- });
-
- it("should delete a specific broadcast message", () => {
- const messageIdToDelete = broadcastMessage1.messageId;
- const channelId = broadcastMessage1.channel;
-
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Broadcast,
- channelId: channelId,
- messageId: messageIdToDelete,
- });
-
- const store = useMessageStore.getState().getMessageStore(123)!;
- expect(store.messages.broadcast.get(channelId)?.get(messageIdToDelete)).toBeUndefined();
- });
-
- it("should clean up empty conversation/channel Maps", () => {
- const directConvId = getConversationId(
- directMessageFromOther1.from,
- directMessageFromOther1.to,
- );
- const broadcastChanId = broadcastMessage1.channel;
-
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Direct,
- nodeA: directMessageToOther1.from,
- nodeB: directMessageToOther1.to,
- messageId: directMessageToOther1.messageId,
- });
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Direct,
- nodeA: directMessageFromOther1.from,
- nodeB: directMessageFromOther1.to,
- messageId: directMessageFromOther1.messageId,
- });
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Direct,
- nodeA: directMessageToOther1.from,
- nodeB: directMessageToOther1.to,
- messageId: extraDirectMessageId,
- });
-
- expect(state.getMessageStore(123)?.messages.direct.has(directConvId)).toBe(false);
-
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Broadcast,
- channelId: broadcastChanId,
- messageId: broadcastMessage1.messageId,
- });
-
- expect(state.getMessageStore(123)?.messages.broadcast.has(broadcastChanId)).toBe(false);
- });
-
- it("should not error when trying to delete non-existent message", () => {
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
- const conversationId = getConversationId(myNodeNum, otherNodeNum1);
-
- expect(() => {
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: otherNodeNum1,
- messageId: 9999,
- });
- }).not.toThrow();
-
- const store = useMessageStore.getState().getMessageStore(123)!;
- const conversationLog = store.messages.direct.get(conversationId);
- expect(conversationLog?.size).toBe(3); // 101, 102, 1011
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("not found in direct chat"));
-
- warnSpy.mockRestore();
- });
-
- it("should not error when trying to delete from non-existent conversation/channel", () => {
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
- expect(() => {
- state.getMessageStore(123)?.clearMessageByMessageId({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: 9998,
- messageId: 101,
- });
- }).not.toThrow();
-
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Message entry"));
-
- expect(warnSpy).toHaveBeenCalledTimes(1);
-
- warnSpy.mockRestore();
- });
- });
-
- describe("Drafts", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- state.addMessageStore(123);
-
- const draftKeyDirect = otherNodeNum1;
- const draftKeyBroadcast = broadcastChannel;
- const draftMessage = "This is a draft";
-
- it("should set and get a draft for direct chat", () => {
- state.getMessageStore(123)?.setDraft(draftKeyDirect, draftMessage);
- expect(state.getMessageStore(123)?.drafts.get(draftKeyDirect)).toBe(draftMessage);
- expect(state.getMessageStore(123)?.getDraft(draftKeyDirect)).toBe(draftMessage);
- });
-
- it("should set and get a draft for broadcast chat", () => {
- state.getMessageStore(123)?.setDraft(draftKeyBroadcast, draftMessage);
- expect(state.getMessageStore(123)?.drafts.get(draftKeyBroadcast)).toBe(draftMessage);
- expect(state.getMessageStore(123)?.getDraft(draftKeyBroadcast)).toBe(draftMessage);
- });
-
- it("should return empty string for non-existent draft", () => {
- expect(state.getMessageStore(123)?.getDraft(999)).toBe("");
- });
-
- it("should clear a draft", () => {
- state.getMessageStore(123)?.setDraft(draftKeyDirect, draftMessage);
- expect(state.getMessageStore(123)?.drafts.has(draftKeyDirect)).toBe(true);
- state.getMessageStore(123)?.clearDraft(draftKeyDirect);
- expect(state.getMessageStore(123)?.drafts.has(draftKeyDirect)).toBe(false);
- expect(state.getMessageStore(123)?.getDraft(draftKeyDirect)).toBe("");
- });
- });
-
- describe("deleteAllMessages", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
- state.addMessageStore(123);
-
- it("should clear all direct and broadcast messages, leaving empty Maps", () => {
- state.getMessageStore(123)?.saveMessage(directMessageToOther1);
- state.getMessageStore(123)?.saveMessage(broadcastMessage1);
-
- expect(state.getMessageStore(123)?.messages.direct.size).toBeGreaterThan(0);
- expect(state.getMessageStore(123)?.messages.broadcast.size).toBeGreaterThan(0);
-
- state.getMessageStore(123)?.deleteAllMessages();
-
- const store = useMessageStore.getState().getMessageStore(123)!;
- expect(store.messages.direct).toBeInstanceOf(Map);
- expect(store.messages.direct.size).toBe(0);
- expect(store.messages.broadcast).toBeInstanceOf(Map);
- expect(store.messages.broadcast.size).toBe(0);
- });
- });
-
- describe("persistence", () => {
- it("partialize persists data; onRehydrateStorage rebuilds methods (messages + drafts survive)", async () => {
- {
- const { useMessageStore } = await freshStore(true);
- const state = useMessageStore.getState();
-
- const store = state.addMessageStore(123);
- store.setNodeNum(321);
-
- const convId = getConversationId(myNodeNum, otherNodeNum1);
- store.saveMessage(directMessageToOther1);
- store.saveMessage(broadcastMessage1);
- store.setDraft(otherNodeNum1 as unknown as Types.Destination, "draft-text");
-
- const store2 = state.addMessageStore(123);
-
- expect(store2.messages.direct.has(convId)).toBe(true);
- expect(store2.messages.direct.get(convId)?.has(101)).toBe(true);
- expect(store2.messages.broadcast.get(broadcastChannel)?.has(201)).toBe(true);
- expect(store2.getDraft(otherNodeNum1 as unknown as Types.Destination)).toBe("draft-text");
- }
- {
- const { useMessageStore } = await freshStore(true);
- const state = useMessageStore.getState();
-
- const store = state.getMessageStore(123)!; // rebuilt instance
- expect(store).toBeTruthy();
-
- // Methods should work after rehydrate
- const directMsgs = store.getMessages({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: otherNodeNum1,
- });
- expect(directMsgs.map((m) => m.messageId)).toEqual([101]);
-
- const bMsgs = store.getMessages({
- type: MessageType.Broadcast,
- channelId: broadcastChannel,
- });
- expect(bMsgs.map((m) => m.messageId)).toEqual([201]);
-
- expect(store.getDraft(otherNodeNum1 as unknown as Types.Destination)).toBe("draft-text");
-
- store.saveMessage(directMessageFromOther1);
-
- const after = store.getMessages({
- type: MessageType.Direct,
- nodeA: myNodeNum,
- nodeB: otherNodeNum1,
- });
- expect(after.map((m) => m.messageId)).toEqual([101, 102]);
- expect(after[1]?.state).toBe(MessageState.Waiting);
- }
- });
-
- it("removeMessageStore persists removal across reload", async () => {
- {
- const { useMessageStore } = await freshStore(true);
- const state = useMessageStore.getState();
- const store = state.addMessageStore(99);
-
- store.setNodeNum(42);
- expect(state.getMessageStore(99)).toBeDefined();
-
- state.removeMessageStore(99);
- expect(state.getMessageStore(99)).toBeUndefined();
- }
- {
- const { useMessageStore } = await freshStore(true);
- const state = useMessageStore.getState();
- expect(state.getMessageStore(99)).toBeUndefined(); // still gone
- }
- });
-
- it("rehydrate only rebuilds stores with myNodeNum set (orphans dropped)", async () => {
- {
- const { useMessageStore } = await freshStore(true);
- const state = useMessageStore.getState();
-
- // Orphan (no myNodeNum)
- const orphan = state.addMessageStore(500);
- orphan.saveMessage(broadcastMessage1);
-
- // Proper store
- const good = state.addMessageStore(501);
- good.setNodeNum(777);
- good.saveMessage(broadcastMessage2);
- }
- {
- const { useMessageStore } = await freshStore(true);
- const state = useMessageStore.getState();
-
- expect(state.getMessageStore(500)).toBeUndefined(); // orphan dropped
- const kept = state.getMessageStore(501);
- expect(kept).toBeDefined();
-
- expect(kept?.messages.broadcast.get(broadcastChannel)?.has(202)).toBe(true);
- }
- });
-
- it("evicts the earliest-added message store when exceeding cap of 10", async () => {
- const { useMessageStore } = await freshStore();
- const state = useMessageStore.getState();
-
- for (let i = 1; i <= 10; i++) {
- state.addMessageStore(i);
- }
- // Adding the 11th should evict the earliest (id=1)
- state.addMessageStore(11);
-
- expect(state.getMessageStore(1)).toBeUndefined(); // evicted
- expect(state.getMessageStore(2)).toBeDefined(); // still there
- expect(state.getMessageStore(11)).toBeDefined(); // newest kept
- });
-
- it("keeps only the latest 1000 messages in a broadcast channel (oldest trimmed)", async () => {
- setAutoFreeze(false); // Disable immer auto-freeze for performance in this test
- try {
- const { useMessageStore, MessageType, MessageState } = await freshStore();
- const state = useMessageStore.getState();
-
- const store = state.addMessageStore(123);
-
- const channelId = 0 as number;
- const total = 1005;
-
- for (let i = 1; i <= total; i++) {
- store.saveMessage({
- type: MessageType.Broadcast,
- from: 123,
- to: 0xffffffff,
- channel: channelId,
- date: Date.now() + i,
- messageId: i,
- state: MessageState.Waiting,
- message: `m${i}`,
- });
- }
-
- const fresh = useMessageStore.getState().getMessageStore(123)!;
- const log = fresh.messages.broadcast.get(channelId)!;
-
- expect(log.size).toBe(1000); // capped
- for (let i = 1; i <= 5; i++) {
- expect(log.has(i)).toBe(false); // oldest removed
- }
- for (let i = 6; i <= 1005; i++) {
- expect(log.has(i)).toBe(true); // newest kept
- }
- } finally {
- setAutoFreeze(true); // Restore immer auto-freeze
- }
- });
- });
-});
diff --git a/packages/web/src/core/stores/messageStore/types.ts b/packages/web/src/core/stores/messageStore/types.ts
deleted file mode 100644
index 120e70b4..00000000
--- a/packages/web/src/core/stores/messageStore/types.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import type { MessageState, MessageType } from "@core/stores";
-import type { Types } from "@meshtastic/sdk";
-
-type NodeNum = number;
-type MessageId = number;
-type ChannelId = Types.ChannelNumber;
-type ConversationId = string;
-type MessageLogMap = Map;
-
-interface MessageBase {
- channel: Types.ChannelNumber;
- to: number;
- from: number;
- date: number;
- messageId: number;
- state: MessageState;
- message: string;
-}
-
-interface GenericMessage extends MessageBase {
- type: T;
-}
-
-type Message = GenericMessage | GenericMessage;
-
-type GetMessagesParams =
- | { type: MessageType.Direct; nodeA: NodeNum; nodeB: NodeNum }
- | { type: MessageType.Broadcast; channelId: ChannelId };
-
-type SetMessageStateParams =
- | {
- type: MessageType.Direct;
- nodeA: NodeNum;
- nodeB: NodeNum;
- messageId: MessageId; // ID of the message within that chat
- newState?: MessageState; // Optional new state, defaults to Ack
- }
- | {
- type: MessageType.Broadcast;
- channelId: ChannelId;
- messageId: MessageId;
- newState?: MessageState; // Optional new state, defaults to Ack
- };
-
-type ClearMessageParams =
- | {
- type: MessageType.Direct;
- nodeA: NodeNum;
- nodeB: NodeNum;
- messageId: MessageId;
- }
- | {
- type: MessageType.Broadcast;
- channelId: ChannelId;
- messageId: MessageId;
- };
-
-export type {
- ChannelId,
- ClearMessageParams,
- ConversationId,
- GetMessagesParams,
- Message,
- MessageId,
- MessageLogMap,
- NodeNum,
- SetMessageStateParams,
-};
diff --git a/packages/web/src/core/subscriptions.ts b/packages/web/src/core/subscriptions.ts
index 56a129d9..245214f7 100644
--- a/packages/web/src/core/subscriptions.ts
+++ b/packages/web/src/core/subscriptions.ts
@@ -1,5 +1,5 @@
import { useNewNodeNum } from "@core/hooks/useNewNodeNum";
-import { type Device, type MessageStore } from "@core/stores";
+import { type Device } from "@core/stores";
import { type MeshDevice, Protobuf } from "@meshtastic/sdk";
/**
@@ -12,12 +12,7 @@ import { type MeshDevice, Protobuf } from "@meshtastic/sdk";
* device-store-only state (waypoints, traceroutes, neighbour info, dialog
* open triggers, unread counts).
*/
-export const subscribeAll = (
- device: Device,
- connection: MeshDevice,
- // biome-ignore lint/correctness/noUnusedFunctionParameters: kept for callsite stability while messageStore is being retired
- _messageStore: MessageStore,
-) => {
+export const subscribeAll = (device: Device, connection: MeshDevice) => {
let myNodeNum = 0;
connection.events.onDeviceMetadataPacket.subscribe((metadataPacket) => {
diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx
index 058f1f0a..e296ab40 100644
--- a/packages/web/src/index.tsx
+++ b/packages/web/src/index.tsx
@@ -10,7 +10,7 @@ import { createRoot } from "react-dom/client";
import "./i18n-config.ts";
import { router } from "@app/routes.tsx";
import { meshRegistry } from "@core/meshRegistry.ts";
-import { useAppStore, useMessageStore } from "@core/stores";
+import { useAppStore } from "@core/stores";
import { MeshRegistryProvider } from "@meshtastic/sdk-react";
import { type createRouter, RouterProvider } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
@@ -26,18 +26,16 @@ const root = createRoot(container);
function IndexPage() {
enableMapSet();
const appStore = useAppStore();
- const messageStore = useMessageStore();
const translation = useTranslation();
const context = React.useMemo(
() => ({
stores: {
app: appStore,
- message: messageStore,
},
i18n: translation,
}),
- [appStore, messageStore, translation],
+ [appStore, translation],
);
return (
diff --git a/packages/web/src/pages/Connections/useConnections.ts b/packages/web/src/pages/Connections/useConnections.ts
index d818e45f..8e443f46 100644
--- a/packages/web/src/pages/Connections/useConnections.ts
+++ b/packages/web/src/pages/Connections/useConnections.ts
@@ -7,7 +7,7 @@ import type {
import { createConnectionFromInput, testHttpReachable } from "@app/pages/Connections/utils";
import { meshRegistry } from "@core/meshRegistry.ts";
import { coordinator, getStorageDb } from "@core/sdkStorage.ts";
-import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores";
+import { useAppStore, useDeviceStore } from "@core/stores";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/sdk";
@@ -40,7 +40,6 @@ export function useConnections() {
const setActiveConnectionId = useDeviceStore((s) => s.setActiveConnectionId);
const { addDevice } = useDeviceStore();
- const { addMessageStore } = useMessageStore();
const { setSelectedDevice } = useAppStore();
const selectedDeviceId = useAppStore((s) => s.selectedDeviceId);
@@ -153,7 +152,6 @@ export function useConnections() {
deviceId = deviceId ?? randId();
const device = addDevice(deviceId);
- const messageStore = addMessageStore(deviceId);
// Wire the SDK slices to the OPFS-backed SQLite repositories so the
// user keeps message + draft + node history across reloads. The DB is
@@ -192,7 +190,7 @@ export function useConnections() {
setSelectedDevice(deviceId);
device.addConnection(meshDevice); // This stores meshDevice in Device.connection
- subscribeAll(device, meshDevice, messageStore);
+ subscribeAll(device, meshDevice);
// Store transport locally for cleanup (BT/Serial only)
if (btDevice || serialPort) {
@@ -266,7 +264,6 @@ export function useConnections() {
[
connections,
addDevice,
- addMessageStore,
setSelectedDevice,
setActiveConnectionId,
updateSavedConnection,
diff --git a/packages/web/src/routes.tsx b/packages/web/src/routes.tsx
index 57a93f8a..542b6df6 100644
--- a/packages/web/src/routes.tsx
+++ b/packages/web/src/routes.tsx
@@ -1,5 +1,5 @@
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
-import type { useAppStore, useMessageStore } from "@core/stores";
+import type { useAppStore } from "@core/stores";
import { Connections } from "@pages/Connections/index.tsx";
import MapPage from "@pages/Map/index.tsx";
import MessagesPage from "@pages/Messages.tsx";
@@ -18,7 +18,6 @@ import { App } from "./App.tsx";
interface AppContext {
stores: {
app: ReturnType;
- message: ReturnType;
};
i18n: ReturnType;
}
@@ -164,7 +163,6 @@ const router = createRouter({
context: {
stores: {
app: {} as ReturnType,
- message: {} as ReturnType,
},
i18n: {} as ReturnType,
},