diff --git a/packages/web/src/core/stores/nodeDBStore/index.ts b/packages/web/src/core/stores/nodeDBStore/index.ts new file mode 100644 index 00000000..9591f65a --- /dev/null +++ b/packages/web/src/core/stores/nodeDBStore/index.ts @@ -0,0 +1,177 @@ +import { createStorageWithMapSupport } from "@core/stores/utils/indexDB.ts"; +import type { Protobuf } from "@meshtastic/core"; +import { produce } from "immer"; +import { create as createStore } from "zustand"; +import { persist } from "zustand/middleware"; +import type { NodeError } from "./types"; + +const CURRENT_STORE_VERSION = 0; + +export interface nodeDB { + userId: number; + nodeMap: Map; + nodeErrors: Map; + + addNode: (nodeInfo: Protobuf.Mesh.NodeInfo) => void; + removeNode: (nodeNum: number) => void; + + getNodesLength: () => number; + getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined; + getNodes: ( + filter?: (node: Protobuf.Mesh.NodeInfo) => boolean, + ) => Protobuf.Mesh.NodeInfo[]; + + setNodeError: (nodeNum: number, error: string) => void; + clearNodeError: (nodeNum: number) => void; + getNodeError: (nodeNum: number) => NodeError | undefined; + hasNodeError: (nodeNum: number) => boolean; +} + +export interface nodeDBState { + addNodeDB: (id: number) => nodeDB; + removeNodeDB: (id: number) => void; + getNodeDBs: () => nodeDB[]; + getNodeDB: (id: number) => nodeDB | undefined; +} + +interface PrivateNodeDBState extends nodeDBState { + nodeDBs: Map; +} + +type NodeDBPersisted = Pick; + +export const useNodeDBStore = createStore()( + persist( + (set, get) => ({ + nodeDBs: new Map(), + addNodeDB: (userId: number) => { + set( + produce((draft) => { + draft.nodeDBs.set(userId, { + userId: userId, + nodeMap: new Map(), + nodeErrors: new Map(), + + addNode: (node: Protobuf.Mesh.NodeInfo) => + set( + produce((draft: PrivateNodeDBState) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + nodeDB.nodeMap.set(node.num, node); + }), + ), + + removeNode: (nodeNum: number) => + set( + produce((draft: PrivateNodeDBState) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + nodeDB.nodeMap.delete(nodeNum); + }), + ), + + setNodeError: (nodeNum: number, error: string) => + set( + produce((draft: PrivateNodeDBState) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + nodeDB.nodeErrors.set(nodeNum, { node: nodeNum, error }); + }), + ), + + clearNodeError: (nodeNum: number) => + set( + produce((draft: PrivateNodeDBState) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + nodeDB.nodeErrors.delete(nodeNum); + }), + ), + + getNodesLength: () => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + return nodeDB.nodeMap.size; + }, + + getNode: (nodeNum: number) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + return nodeDB.nodeMap.get(nodeNum); + }, + + getNodes: (filter?) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + const allNodes = Array.from(nodeDB.nodeMap.values()).filter( + (node) => node.num !== nodeDB.userId, + ); + if (filter) { + return allNodes.filter(filter); + } + return allNodes; + }, + + getNodeError: (nodeNum: number) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + return nodeDB.nodeErrors.get(nodeNum); + }, + + hasNodeError: (nodeNum: number) => { + const nodeDB = draft.nodeDBs.get(userId); + if (!nodeDB) { + throw new Error(`NodeDB for userID ${userId} not found`); + } + return nodeDB.nodeErrors.has(nodeNum) ?? false; + }, + }); + }), + ); + + const nodeDB = get().nodeDBs.get(userId); + + if (!nodeDB) { + throw new Error( + `Failed to create or retrieve nodeDB for userID ${userId}`, + ); + } + return nodeDB; + }, + removeNodeDB: (userId) => { + set( + produce((draft) => { + draft.nodeDBs.delete(userId); + }), + ); + }, + + getNodeDBs: () => Array.from(get().nodeDBs.values()), + getNodeDB: (userId) => get().nodeDBs.get(userId), + }), + { + name: "meshtastic-nodedb-store", + storage: createStorageWithMapSupport(), + version: CURRENT_STORE_VERSION, + partialize: (s) => ({ + nodeDBs: s.nodeDBs, + }), + }, + ), +); diff --git a/packages/web/src/core/stores/nodeDBStore/types.ts b/packages/web/src/core/stores/nodeDBStore/types.ts new file mode 100644 index 00000000..cba10d51 --- /dev/null +++ b/packages/web/src/core/stores/nodeDBStore/types.ts @@ -0,0 +1,6 @@ +type NodeError = { + node: number; + error: string; +}; + +export type { NodeError }; diff --git a/packages/web/src/core/stores/utils/indexDB.ts b/packages/web/src/core/stores/utils/indexDB.ts index 26d219d3..994bbaa3 100644 --- a/packages/web/src/core/stores/utils/indexDB.ts +++ b/packages/web/src/core/stores/utils/indexDB.ts @@ -1,7 +1,3 @@ -import type { - ChannelId, - MessageLogMap, -} from "@core/stores/messageStore/types.ts"; import { del, get, set } from "idb-keyval"; import type { PersistStorage, @@ -9,14 +5,6 @@ import type { StorageValue, } from "zustand/middleware"; -type PersistedMessageState = { - messages: { - direct: Map; - broadcast: Map; - }; - nodeNum: number; -}; - export const zustandIndexDBStorage: StateStorage = { getItem: async (name: string): Promise => { return (await get(name)) || null; @@ -62,40 +50,34 @@ const reviver: JsonReviver = (_, value) => { return value; }; -export const storageWithMapSupport: PersistStorage = { - getItem: async ( - name, - ): Promise | null> => { - const str = await zustandIndexDBStorage.getItem(name); - if (!str) { - return null; - } - try { - const parsed = JSON.parse( - str, - reviver, - ) as StorageValue; - return parsed; - } catch (error) { - console.error(`Error parsing persisted state (${name}):`, error); - return null; - } - }, - setItem: async ( - name, - newValue: StorageValue, - ): Promise => { - try { - const str = JSON.stringify(newValue, replacer); - await zustandIndexDBStorage.setItem(name, str); - } catch (error) { - console.error( - `Error stringifying or setting persisted state (${name}):`, - error, - ); - } - }, - removeItem: async (name): Promise => { - await zustandIndexDBStorage.removeItem(name); - }, -}; +export function createStorageWithMapSupport(): PersistStorage { + return { + getItem: async (name): Promise | null> => { + const str = await zustandIndexDBStorage.getItem(name); + if (!str) { + return null; + } + try { + const parsed = JSON.parse(str, reviver) as StorageValue; + return parsed; + } catch (error) { + console.error(`Error parsing persisted state (${name}):`, error); + return null; + } + }, + setItem: async (name, newValue: StorageValue): Promise => { + try { + const str = JSON.stringify(newValue, replacer); + await zustandIndexDBStorage.setItem(name, str); + } catch (error) { + console.error( + `Error stringifying or setting persisted state (${name}):`, + error, + ); + } + }, + removeItem: async (name): Promise => { + await zustandIndexDBStorage.removeItem(name); + }, + }; +}