diff --git a/packages/sdk/src/features/nodes/NodesClient.test.ts b/packages/sdk/src/features/nodes/NodesClient.test.ts index bb37d4a4..90eb3112 100644 --- a/packages/sdk/src/features/nodes/NodesClient.test.ts +++ b/packages/sdk/src/features/nodes/NodesClient.test.ts @@ -25,4 +25,57 @@ describe("NodesClient", () => { const client = new MeshClient({ transport }); expect(client.nodes.byNum(999)).toBeUndefined(); }); + + it("seeds a self-node stub from onMyNodeInfo when no NodeInfo has arrived", () => { + const { transport } = createFakeTransport(); + const client = new MeshClient({ transport }); + + client.events.onMyNodeInfo.dispatch(create(Protobuf.Mesh.MyNodeInfoSchema, { myNodeNum: 42 })); + + const seeded = client.nodes.byNum(42); + expect(seeded).toBeDefined(); + expect(seeded?.num).toBe(42); + expect(seeded?.user).toBeUndefined(); + expect(seeded?.isFavorite).toBe(false); + expect(seeded?.isIgnored).toBe(false); + }); + + it("does not overwrite a richer NodeInfo entry with a later MyNodeInfo seed", () => { + const { transport } = createFakeTransport(); + const client = new MeshClient({ transport }); + + client.events.onNodeInfoPacket.dispatch( + create(Protobuf.Mesh.NodeInfoSchema, { + num: 42, + user: create(Protobuf.Mesh.UserSchema, { id: "!000a", longName: "Alice" }), + }), + ); + client.events.onMyNodeInfo.dispatch(create(Protobuf.Mesh.MyNodeInfoSchema, { myNodeNum: 42 })); + + const node = client.nodes.byNum(42); + expect(node?.user?.longName).toBe("Alice"); + }); + + it("lets a real NodeInfo patch over a seeded self-node stub", () => { + const { transport } = createFakeTransport(); + const client = new MeshClient({ transport }); + + client.events.onMyNodeInfo.dispatch(create(Protobuf.Mesh.MyNodeInfoSchema, { myNodeNum: 42 })); + client.events.onNodeInfoPacket.dispatch( + create(Protobuf.Mesh.NodeInfoSchema, { + num: 42, + user: create(Protobuf.Mesh.UserSchema, { id: "!000a", longName: "Alice" }), + }), + ); + + const node = client.nodes.byNum(42); + expect(node?.user?.longName).toBe("Alice"); + }); + + it("ignores onMyNodeInfo with myNodeNum=0", () => { + const { transport } = createFakeTransport(); + const client = new MeshClient({ transport }); + client.events.onMyNodeInfo.dispatch(create(Protobuf.Mesh.MyNodeInfoSchema, { myNodeNum: 0 })); + expect(client.nodes.list.value).toEqual([]); + }); }); diff --git a/packages/sdk/src/features/nodes/NodesClient.ts b/packages/sdk/src/features/nodes/NodesClient.ts index 9ef7e5d3..08d1425c 100644 --- a/packages/sdk/src/features/nodes/NodesClient.ts +++ b/packages/sdk/src/features/nodes/NodesClient.ts @@ -44,6 +44,25 @@ export class NodesClient { client.events.onNodeInfoPacket.subscribe((info) => this.handleIncoming(info)); + // Seed a self-node stub from MyNodeInfo. Firmware always emits MyNodeInfo + // during the wantConfigId handshake, but not every session includes a + // NodeInfo for the device itself in the config bundle (fresh flash, + // cleared nodeDB, etc.). Without this seed, consumers that look up + // `nodes.list.find(n => n.num === myNodeNum)` — notably the web sidebar — + // sit empty forever. The real NodeInfo / UserPacket / PositionPacket + // handlers above patch the stub when they arrive. + client.events.onMyNodeInfo.subscribe((info) => { + if (info.myNodeNum === 0) return; + if (this.store.has(info.myNodeNum)) return; + const seeded: Node = { + num: info.myNodeNum, + isFavorite: false, + isIgnored: false, + }; + this.store.set(info.myNodeNum, seeded); + void this.repository.upsert(seeded).catch(() => {}); + }); + client.events.onUserPacket.subscribe((packet) => { this.patch(packet.from, { user: packet.data }); });