Migrate the /api/regions endpoint to ConnectRPC

main added a dynamically-discovered regions endpoint (region IATA system).
Port it to a RegionsService.GetRegions RPC: new regions.proto (Region /
RegionGroup messages), a handler reusing getAvailableRegions()/getRegionGroups(),
and useRegions/useRegionGroups switched to connect-query against the generated
types. The REST /api/regions route stays for coexistence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Vanderpot
2026-06-14 17:49:18 -04:00
parent c6d688c931
commit 8416910fc9
4 changed files with 64 additions and 28 deletions
@@ -0,0 +1,31 @@
syntax = "proto3";
package meshexplorer.v1;
// A dynamically-discovered region (meshcore_regions MV).
message Region {
// Canonical IATA region code, e.g. "SEA".
string name = 1;
string friendly_name = 2;
// Distinct nodes seen in this region (last 30 days).
int32 node_count = 3;
string last_seen = 4;
}
// A named set of IATA region codes (region_groups table).
message RegionGroup {
string code = 1;
string name = 2;
repeated string members = 3;
}
message GetRegionsRequest {}
message GetRegionsResponse {
repeated Region regions = 1;
repeated RegionGroup groups = 2;
}
service RegionsService {
rpc GetRegions(GetRegionsRequest) returns (GetRegionsResponse);
}
+7 -28
View File
@@ -1,37 +1,16 @@
import { useQuery } from "@tanstack/react-query";
import { buildApiUrl } from "@/lib/api";
import type { RegionGroup } from "@/lib/regionGroups";
export interface RegionOption {
name: string;
friendlyName: string;
nodeCount?: number;
lastSeen?: string;
}
interface RegionsResponse {
regions: RegionOption[];
groups: RegionGroup[];
}
import { useQuery } from "@connectrpc/connect-query";
import { RegionsService } from "@/gen/meshexplorer/v1/regions_pb";
const STALE_TIME = 60 * 60 * 1000; // 1h — the region set is slow-moving / MV-backed
const GC_TIME = 2 * 60 * 60 * 1000;
// Single shared query (one network request) feeding both useRegions() and useRegionGroups().
function useRegionsQuery() {
return useQuery<RegionsResponse>({
queryKey: ["regions"],
queryFn: async ({ signal }) => {
const response = await fetch(buildApiUrl("/api/regions"), { signal });
if (!response.ok) {
throw new Error(`Failed to fetch regions: ${response.statusText}`);
}
return response.json();
},
staleTime: STALE_TIME,
gcTime: GC_TIME,
retry: 2,
});
return useQuery(
RegionsService.method.getRegions,
{},
{ staleTime: STALE_TIME, gcTime: GC_TIME, retry: 2 },
);
}
export function useRegions() {
@@ -0,0 +1,23 @@
import type { ServiceImpl } from "@connectrpc/connect";
import { RegionsService } from "@/gen/meshexplorer/v1/regions_pb";
import { getAvailableRegions, getRegionGroups } from "@/lib/clickhouse/regions";
import { num } from "./mappers";
export const regionsServiceImpl: ServiceImpl<typeof RegionsService> = {
async getRegions() {
const [regions, groups] = await Promise.all([getAvailableRegions(), getRegionGroups()]);
return {
regions: regions.map((r) => ({
name: r.name,
friendlyName: r.friendlyName,
nodeCount: num(r.nodeCount),
lastSeen: r.lastSeen,
})),
groups: groups.map((g) => ({
code: g.code,
name: g.name,
members: g.members,
})),
};
},
};
@@ -5,12 +5,14 @@ import { NodeService } from "@/gen/meshexplorer/v1/node_pb";
import { StatsService } from "@/gen/meshexplorer/v1/stats_pb";
import { ChatService } from "@/gen/meshexplorer/v1/chat_pb";
import { PacketsService } from "@/gen/meshexplorer/v1/packets_pb";
import { RegionsService } from "@/gen/meshexplorer/v1/regions_pb";
import { mapServiceImpl } from "./map";
import { neighborsServiceImpl } from "./neighbors";
import { nodeServiceImpl } from "./node";
import { statsServiceImpl } from "./stats";
import { chatServiceImpl } from "./chat";
import { packetsServiceImpl } from "./packets";
import { regionsServiceImpl } from "./regions";
export default function routes(router: ConnectRouter) {
router.service(MapService, mapServiceImpl);
@@ -19,4 +21,5 @@ export default function routes(router: ConnectRouter) {
router.service(StatsService, statsServiceImpl);
router.service(ChatService, chatServiceImpl);
router.service(PacketsService, packetsServiceImpl);
router.service(RegionsService, regionsServiceImpl);
}