From 092b90b989aed55b089ccb2e14a523dd131c163a Mon Sep 17 00:00:00 2001 From: philon- Date: Sun, 21 Sep 2025 15:28:21 +0200 Subject: [PATCH] Additional waypoint methods, update mock, update tests --- .../stores/deviceStore/deviceStore.mock.ts | 9 ++ .../stores/deviceStore/deviceStore.test.ts | 39 +++--- .../web/src/core/stores/deviceStore/index.ts | 112 ++++++++++++++---- 3 files changed, 123 insertions(+), 37 deletions(-) diff --git a/packages/web/src/core/stores/deviceStore/deviceStore.mock.ts b/packages/web/src/core/stores/deviceStore/deviceStore.mock.ts index 04b832d1..d3fd2255 100644 --- a/packages/web/src/core/stores/deviceStore/deviceStore.mock.ts +++ b/packages/web/src/core/stores/deviceStore/deviceStore.mock.ts @@ -45,8 +45,13 @@ export const mockDeviceStore: Device = { deleteMessages: false, managedMode: false, clientNotification: false, + resetNodeDb: false, + clearAllStores: false, + factoryResetConfig: false, + factoryResetDevice: false, }, clientNotifications: [], + neighborInfo: new Map(), setStatus: vi.fn(), setConfig: vi.fn(), @@ -67,6 +72,8 @@ export const mockDeviceStore: Device = { setPendingSettingsChanges: vi.fn(), addChannel: vi.fn(), addWaypoint: vi.fn(), + removeWaypoint: vi.fn(), + getWaypoint: vi.fn(), addConnection: vi.fn(), addTraceRoute: vi.fn(), addMetadata: vi.fn(), @@ -81,4 +88,6 @@ export const mockDeviceStore: Device = { getClientNotification: vi.fn(), getAllUnreadCount: vi.fn().mockReturnValue(0), getUnreadCount: vi.fn().mockReturnValue(0), + getNeighborInfo: vi.fn(), + addNeighborInfo: vi.fn(), }; diff --git a/packages/web/src/core/stores/deviceStore/deviceStore.test.ts b/packages/web/src/core/stores/deviceStore/deviceStore.test.ts index 93f94058..8348b2b0 100644 --- a/packages/web/src/core/stores/deviceStore/deviceStore.test.ts +++ b/packages/web/src/core/stores/deviceStore/deviceStore.test.ts @@ -349,19 +349,16 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa // Old device with myNodeNum=777 and some waypoints (one expired) const oldDevice = state.addDevice(1); + oldDevice.connection = { sendWaypoint: vi.fn() } as any; + oldDevice.setHardware(makeHardware(777)); oldDevice.addWaypoint( - makeWaypoint(1, Date.parse("2024-12-31T23:59:59Z")), + makeWaypoint(1, Date.parse("2024-12-31T23:59:59Z")), // This is expired, will not be added 0, 0, new Date(), ); // expired - oldDevice.addWaypoint( - makeWaypoint(2, Date.parse("2028-01-01T00:00:00Z")), - 0, - 0, - new Date(), - ); // ok + oldDevice.addWaypoint(makeWaypoint(2, 0), 0, 0, new Date()); // no expire oldDevice.addWaypoint( makeWaypoint(3, Date.parse("2026-01-01T00:00:00Z")), 0, @@ -380,12 +377,12 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa ); const wps = useDeviceStore.getState().devices.get(1)!.waypoints; - expect(wps.length).toBe(3); + expect(wps.length).toBe(2); expect(wps.find((w) => w.id === 2)?.expire).toBe( Date.parse("2027-01-01T00:00:00Z"), ); - // Retention: push 102 total waypoints -> capped at 100. Oldest (id=1,2) evicted + // Retention: push 102 total waypoints -> capped at 100. Oldest evicted for (let i = 3; i <= 102; i++) { oldDevice.addWaypoint(makeWaypoint(i), 0, 0, new Date()); } @@ -394,24 +391,32 @@ describe("DeviceStore – traceroutes & waypoints retention + merge on setHardwa 100, ); + // Remove waypoint + oldDevice.removeWaypoint(102, false); + expect(oldDevice.connection?.sendWaypoint).not.toHaveBeenCalled(); + + await oldDevice.removeWaypoint(101, true); // toMesh=true + expect(oldDevice.connection?.sendWaypoint).toHaveBeenCalled(); + + expect(useDeviceStore.getState().devices.get(1)!.waypoints.length).toBe(98); + // New device shares myNodeNum; setHardware should: // - move traceroutes from old device // - copy waypoints minus expired // - delete old device entry - const newDev = state.addDevice(2); - newDev.setHardware(makeHardware(777)); + const newDevice = state.addDevice(2); + newDevice.setHardware(makeHardware(777)); expect(state.getDevice(1)).toBeUndefined(); expect(state.getDevice(2)).toBeDefined(); // traceroutes moved: - expect(state.getDevice(2)!.traceroutes.size).toBeGreaterThan(0); + expect(state.getDevice(2)!.traceroutes.size).toBe(2); - // expired waypoint removed, last non-expired retained: - const newWps = state.getDevice(2)!.waypoints; - expect(newWps.find((w) => w.id === 1)).toBeUndefined(); - expect(newWps.find((w) => w.id === 2)).toBeUndefined(); - expect(newWps.find((w) => w.id === 3)).toBeTruthy(); + // Getter for waypoint by id works + expect(newDevice.getWaypoint(1)).toBeUndefined(); + expect(newDevice.getWaypoint(2)).toBeUndefined(); + expect(newDevice.getWaypoint(3)).toBeTruthy(); vi.useRealTimers(); }); diff --git a/packages/web/src/core/stores/deviceStore/index.ts b/packages/web/src/core/stores/deviceStore/index.ts index 7968e7f0..f57f7f92 100644 --- a/packages/web/src/core/stores/deviceStore/index.ts +++ b/packages/web/src/core/stores/deviceStore/index.ts @@ -89,6 +89,8 @@ export interface Device extends DeviceData { from: number, rxTime: Date, ) => void; + removeWaypoint: (waypointId: number, toMesh: boolean) => Promise; + getWaypoint: (waypointId: number) => WaypointWithMetadata | undefined; addConnection: (connection: MeshDevice) => void; addTraceRoute: ( traceroute: Types.PacketMetadata, @@ -566,32 +568,102 @@ function deviceFactory( set( produce((draft) => { const device = draft.devices.get(id); - if (device) { - const index = device.waypoints.findIndex( - (wp) => wp.id === waypoint.id, - ); - if (index !== -1) { - const created = - device.waypoints[index]?.metadata.created ?? new Date(); - const updatedWaypoint = { - ...waypoint, - metadata: { created, updated: rxTime, from, channel }, - }; - - device.waypoints[index] = updatedWaypoint; - } else { - device.waypoints.push({ - ...waypoint, - metadata: { created: rxTime, from, channel }, - }); + if (!device) { + return undefined; + } + + const index = device.waypoints.findIndex( + (wp) => wp.id === waypoint.id, + ); + + if (index !== -1) { + const created = + device.waypoints[index]?.metadata.created ?? new Date(); + const updatedWaypoint = { + ...waypoint, + metadata: { created, updated: rxTime, from, channel }, + }; + + // Remove existing waypoint + device.waypoints.splice(index, 1); + + // Push new if no expiry or not expired + if (waypoint.expire === 0 || waypoint.expire > Date.now()) { + device.waypoints.push(updatedWaypoint); } + } else if ( + // only add if set to never expire or not already expired + waypoint.expire === 0 || + (waypoint.expire !== 0 && waypoint.expire < Date.now()) + ) { + device.waypoints.push({ + ...waypoint, + metadata: { created: rxTime, from, channel }, + }); + } + + // Enforce retention limit + evictOldestEntries(device.waypoints, WAYPOINT_RETENTION_NUM); + }), + ); + }, + removeWaypoint: async (waypointId: number, toMesh: boolean) => { + const device = get().devices.get(id); + if (!device) { + return; + } + + const waypoint = device.waypoints.find((wp) => wp.id === waypointId); + if (!waypoint) { + return; + } - // Enforce retention limit - evictOldestEntries(device.waypoints, WAYPOINT_RETENTION_NUM); + if (toMesh) { + if (!device.connection) { + return; + } + + const waypointToBroadcast = create(Protobuf.Mesh.WaypointSchema, { + id: waypoint.id, // Bare minimum to delete a waypoint + lockedTo: 0, + name: "", + description: "", + icon: 0, + expire: 1, + }); + + await device.connection.sendWaypoint( + waypointToBroadcast, + "broadcast", + waypoint.metadata.channel, + ); + } + + // Remove from store + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) { + return; + } + + const idx = device.waypoints.findIndex( + (waypoint) => waypoint.id === waypointId, + ); + if (idx >= 0) { + device.waypoints.splice(idx, 1); } }), ); }, + getWaypoint: (waypointId: number) => { + const device = get().devices.get(id); + if (!device) { + return; + } + + return device.waypoints.find((waypoint) => waypoint.id === waypointId); + }, setActiveNode: (node) => { set( produce((draft) => {