feat: added message persistance
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import type { Types } from "@meshtastic/js";
|
||||
import { Message, MessageType, MessageState } from "@core/stores/messageStore.ts";
|
||||
|
||||
class PacketToMessageDTO {
|
||||
channel: Types.ChannelNumber;
|
||||
to: number;
|
||||
from: number;
|
||||
date: string;
|
||||
messageId: number;
|
||||
state: MessageState;
|
||||
message: string;
|
||||
type: MessageType;
|
||||
|
||||
constructor(data: Types.PacketMetadata<string>, nodeNum: number) {
|
||||
const payload = data
|
||||
|
||||
this.channel = payload.channel
|
||||
this.to = payload.to;
|
||||
this.from = payload.from;
|
||||
this.date = new Date(payload.rxTime).toISOString();
|
||||
this.messageId = payload.id;
|
||||
this.state = payload.from !== nodeNum ? "ack" : "waiting";
|
||||
this.message = payload.data;
|
||||
this.type = payload.type;
|
||||
}
|
||||
|
||||
toMessage(): Message {
|
||||
return {
|
||||
channel: this.channel,
|
||||
to: this.to,
|
||||
from: this.from,
|
||||
date: this.date,
|
||||
messageId: this.messageId,
|
||||
state: this.state,
|
||||
message: this.message,
|
||||
type: this.type,
|
||||
} as Message;
|
||||
}
|
||||
}
|
||||
|
||||
export default PacketToMessageDTO;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StateStorage } from "zustand/middleware";
|
||||
import { get, set, del } from "idb-keyval";
|
||||
|
||||
export const zustandIDBStorage: StateStorage = {
|
||||
export const zustandIndexDBStorage: StateStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
return (await get(name)) || null;
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface MessageWithState extends Types.PacketMetadata<string> {
|
||||
state: MessageState;
|
||||
}
|
||||
|
||||
export type MessageState = "ack" | "waiting" | Protobuf.Mesh.Routing_Error;
|
||||
export type MessageState = "ack" | "waiting" | 'failed';
|
||||
|
||||
export interface ProcessPacketParams {
|
||||
from: number;
|
||||
|
||||
@@ -2,40 +2,60 @@ import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { produce } from 'immer';
|
||||
import { Types } from '@meshtastic/core';
|
||||
import { zustandIDBStorage } from "@core/services/messaging/db.ts";
|
||||
import { zustandIndexDBStorage } from "@core/services/messaging/db.ts";
|
||||
|
||||
export interface MessageWithState {
|
||||
id: number;
|
||||
from: number;
|
||||
const MESSAGE_STATES = {
|
||||
ack: "ack",
|
||||
waiting: "waiting",
|
||||
failed: 'failed',
|
||||
};
|
||||
export type MessageState = keyof typeof MESSAGE_STATES;
|
||||
|
||||
export const ChatTypes = {
|
||||
DIRECT: "direct",
|
||||
BROADCAST: "broadcast",
|
||||
} as const;
|
||||
|
||||
export type MessageType = "broadcast" | "direct";
|
||||
|
||||
interface MessageBase {
|
||||
channel: Types.ChannelNumber;
|
||||
to: number;
|
||||
channel: number;
|
||||
content: string;
|
||||
state: 'ack' | 'waiting' | 'failed';
|
||||
type: 'direct' | 'broadcast';
|
||||
from: number;
|
||||
date: string;
|
||||
messageId: number;
|
||||
state: MessageState;
|
||||
message: string;
|
||||
}
|
||||
|
||||
type MessageType = 'direct' | 'broadcast';
|
||||
interface GenericMessage<T extends MessageType> extends MessageBase {
|
||||
type: T;
|
||||
}
|
||||
|
||||
export type Message = GenericMessage<'direct'> | GenericMessage<'broadcast'>;
|
||||
|
||||
export interface MessageStore {
|
||||
messages: {
|
||||
direct: Record<number, MessageWithState[]>;
|
||||
broadcast: Record<number, MessageWithState[]>;
|
||||
direct: Record<number, Record<number, Message>>; // node -> messageId -> Message
|
||||
broadcast: Record<number, Record<number, Message>>; // channel -> messageId -> Message
|
||||
};
|
||||
|
||||
nodeNum: number;
|
||||
activeChat: number;
|
||||
chatType: MessageType;
|
||||
|
||||
setNodeNum: (nodeNum: number) => void;
|
||||
getNodeNum: () => number;
|
||||
setActiveChat: (chat: number) => void;
|
||||
setChatType: (type: MessageType) => void;
|
||||
addMessage: (message: MessageWithState) => void;
|
||||
getMessages: (type: MessageType, key: number) => MessageWithState[];
|
||||
setMessageState: (
|
||||
type: MessageType,
|
||||
key: number,
|
||||
messageId: number,
|
||||
newState: MessageWithState['state']
|
||||
) => void;
|
||||
saveMessage: (message: Message) => void;
|
||||
setMessageState: (params: {
|
||||
type: MessageType;
|
||||
key: number;
|
||||
messageId: number;
|
||||
newState?: MessageState;
|
||||
}) => void;
|
||||
clearMessages: () => void;
|
||||
getMessages: (type: MessageType, options: { myNodeNum?: number; otherNodeNum?: number; channel?: number }) => Message[];
|
||||
}
|
||||
|
||||
export const useMessageStore = create<MessageStore>()(
|
||||
@@ -45,9 +65,16 @@ export const useMessageStore = create<MessageStore>()(
|
||||
direct: {},
|
||||
broadcast: {},
|
||||
},
|
||||
|
||||
activeChat: Types.ChannelNumber.Primary,
|
||||
activeChat: 0,
|
||||
chatType: 'broadcast',
|
||||
nodeNum: 0,
|
||||
setNodeNum: (nodeNum) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
state.nodeNum = nodeNum;
|
||||
}));
|
||||
},
|
||||
|
||||
getNodeNum: () => get().nodeNum,
|
||||
|
||||
setActiveChat: (chat) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
@@ -61,48 +88,62 @@ export const useMessageStore = create<MessageStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
addMessage: (message) => {
|
||||
saveMessage: (message) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
const group = message.type === 'direct' ? state.messages.direct : state.messages.broadcast;
|
||||
const key = message.type === 'direct' ? message.from : message.channel;
|
||||
const group = state.messages[message.type];
|
||||
const key = message.type === 'direct' ? Number(message.from) : Number(message.channel);
|
||||
|
||||
if (!group[key]) {
|
||||
group[key] = [];
|
||||
group[key] = {};
|
||||
}
|
||||
group[key].push(message);
|
||||
group[key][message.messageId] = message;
|
||||
}));
|
||||
},
|
||||
|
||||
getMessages: (type, key) => {
|
||||
const group = type === 'direct' ? get().messages.direct : get().messages.broadcast;
|
||||
return group[key] ?? [];
|
||||
},
|
||||
|
||||
setMessageState: (type, key, messageId, newState) => {
|
||||
setMessageState: ({ type, key, messageId, newState = 'ack' }) => {
|
||||
set(produce((state: MessageStore) => {
|
||||
const group = type === 'direct' ? state.messages.direct : state.messages.broadcast;
|
||||
const messages = group[key];
|
||||
if (!messages) return;
|
||||
const message = messages.find((msg) => msg.id === messageId);
|
||||
if (message) {
|
||||
message.state = newState;
|
||||
}
|
||||
const group = state.messages[type];
|
||||
const messageMap = group[key];
|
||||
if (!messageMap || !messageMap[messageId]) return;
|
||||
messageMap[messageId].state = newState;
|
||||
}));
|
||||
},
|
||||
|
||||
clearMessages: () => {
|
||||
set(produce((state: MessageStore) => {
|
||||
state.messages.direct = {};
|
||||
state.messages.broadcast = {};
|
||||
}));
|
||||
},
|
||||
getMessages: (type, options) => {
|
||||
const state = get();
|
||||
|
||||
if (type === 'broadcast' && options.channel !== undefined) {
|
||||
const messageMap = state.messages.broadcast[options.channel] ?? {};
|
||||
return Object.values(messageMap).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
}
|
||||
|
||||
if (type === 'direct' && options.myNodeNum !== undefined && options.otherNodeNum !== undefined) {
|
||||
const receivedMap = state.messages.direct[options.otherNodeNum] ?? {};
|
||||
const sentMap = state.messages.direct[options.myNodeNum] ?? {};
|
||||
|
||||
// Pull messages where I am the sender and otherNode is the receiver
|
||||
const sentMessages = Object.values(sentMap).filter(msg => msg.to === options.otherNodeNum);
|
||||
|
||||
// Pull messages received from otherNode
|
||||
const receivedMessages = Object.values(receivedMap);
|
||||
|
||||
// Merge and sort chronologically
|
||||
return [...receivedMessages, ...sentMessages].sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'mesh-messages',
|
||||
storage: createJSONStorage(() => zustandIDBStorage),
|
||||
// ✅ No need for partialize magic — simple object storage
|
||||
name: 'meshtastic-message-store',
|
||||
storage: createJSONStorage(() => zustandIndexDBStorage),
|
||||
partialize: (state) => ({
|
||||
activeChat: state.activeChat,
|
||||
chatType: state.chatType,
|
||||
messages: state.messages,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Device } from "@core/stores/deviceStore.ts";
|
||||
import { MeshDevice, Protobuf } from "@meshtastic/core";
|
||||
import type { MessageStore } from "@core/stores/messageStore.ts";
|
||||
import PacketToMessageDTO from "@core/dto/PacketToMessageDTO.ts";
|
||||
|
||||
export const subscribeAll = (
|
||||
device: Device,
|
||||
@@ -17,7 +18,7 @@ export const subscribeAll = (
|
||||
});
|
||||
|
||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||
console.log("routingPacket", routingPacket);
|
||||
console.log("Routing Packet", routingPacket);
|
||||
|
||||
switch (routingPacket.data.variant.case) {
|
||||
case "errorReason": {
|
||||
@@ -55,6 +56,7 @@ export const subscribeAll = (
|
||||
|
||||
connection.events.onMyNodeInfo.subscribe((nodeInfo) => {
|
||||
device.setHardware(nodeInfo);
|
||||
messageStore.setNodeNum(nodeInfo.myNodeNum);
|
||||
myNodeNum = nodeInfo.myNodeNum;
|
||||
});
|
||||
|
||||
@@ -85,12 +87,13 @@ export const subscribeAll = (
|
||||
|
||||
|
||||
connection.events.onMessagePacket.subscribe((messagePacket) => {
|
||||
console.log("messagePacket", messagePacket);
|
||||
messageStore.addMessage({
|
||||
...messagePacket,
|
||||
console.log("before Message Packet", messagePacket);
|
||||
|
||||
state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
|
||||
});
|
||||
// incoming and outgoing messages are handled by this event listener
|
||||
const dto = new PacketToMessageDTO(messagePacket, myNodeNum);
|
||||
const message = dto.toMessage();
|
||||
console.log("after Message Packet", message);
|
||||
messageStore.saveMessage(message);
|
||||
});
|
||||
|
||||
connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => {
|
||||
@@ -111,9 +114,6 @@ export const subscribeAll = (
|
||||
});
|
||||
});
|
||||
|
||||
// connection.events.onQueueStatus.subscribe((queueStatus) => {
|
||||
// device.setQueueStatus(queueStatus);
|
||||
// });
|
||||
|
||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||
if (routingPacket.data.variant.case === "errorReason") {
|
||||
|
||||
Reference in New Issue
Block a user