fix(pathing): expose canonical path metadata in public DTOs

This commit is contained in:
gadgethd committed 2026-08-03 15:13:11 +01:00
1 parent b2c46f7103
commit 457eb77cbb
2 files changed
+126 -2

No files matched your search

+62 -2
View File
@@ -414,11 +414,27 @@ function findSharedPathPrefix(pathsByObserver: readonly string[][]): string[] {
return first.slice(0, length);
}
export type BetaCanonicalPathNode = {
position: number;
hash: string;
nodeId: string | null;
name: string | null;
lat: number | null;
lon: number | null;
ambiguous: boolean;
confidence: number | null;
};
export type BetaPathObserver = { observerId: string };
export type BetaResolvedPayload = {
ok: boolean;
packetHash: string;
mode: 'resolved' | 'none';
confidence: number | null;
canonicalPath: BetaCanonicalPathNode[];
observers: BetaPathObserver[];
network: string;
permutationCount: number;
remainingHops: number | null;
purplePath: [number, number][] | null;
@@ -492,6 +508,25 @@ function maskPath(
return path.map((point) => hiddenCoordMask.get(hiddenCoordKey(point[0], point[1])) ?? point);
}
function buildCanonicalPath(decoded: BetaSharedDecode): BetaCanonicalPathNode[] {
const nodes: BetaCanonicalPathNode[] = [];
for (let position = 0; position < decoded.canonicalHashes.length; position++) {
const hop = decoded.hops.get(position);
const confidence = decoded.hopConfidences.get(position) ?? null;
nodes.push({
position,
hash: decoded.canonicalHashes[position] ?? hop?.hash ?? '',
nodeId: hop?.nodeId ?? null,
name: hop?.name ?? null,
lat: hop?.lat ?? null,
lon: hop?.lon ?? null,
ambiguous: hop?.ambiguous ?? (confidence == null || confidence < BETA_PURPLE_THRESHOLD),
confidence,
});
}
return nodes;
}
function maskResolvedPayload(
payload: BetaResolvedPayload,
hiddenCoordMask: ReadonlyMap<string, [number, number]>,
@@ -539,6 +574,9 @@ function emptyPayload(
packetHash,
mode: 'none',
confidence: null,
canonicalPath: [],
observers: [],
network: '',
permutationCount: 0,
remainingHops,
purplePath: null,
@@ -561,6 +599,7 @@ function payloadFromProjection(
packetHash: string,
entry: BetaObserverEntry,
projection: BetaPathProjection,
canonical: { canonicalPath: BetaCanonicalPathNode[]; observers: BetaPathObserver[]; network: string },
): BetaResolvedPayload {
if (!projection.purplePath) {
return emptyPayload(packetHash, entry.packet, entry.hashes.length, projection.remainingHops);
@@ -570,6 +609,9 @@ function payloadFromProjection(
packetHash,
mode: 'resolved',
confidence: projection.confidence,
canonicalPath: canonical.canonicalPath,
observers: canonical.observers,
network: canonical.network,
permutationCount: 0,
remainingHops: projection.remainingHops,
purplePath: projection.purplePath,
@@ -670,7 +712,11 @@ export async function resolveBetaPathForPacketHash(
? context.nodesById.get(selectedEntry.packet.src_node_id) ?? null
: null;
const projection = projectCanonicalPathForObserver(selectedEntry, decoded, context, source);
const payload = payloadFromProjection(packetHash, selectedEntry, projection);
const payload = payloadFromProjection(packetHash, selectedEntry, projection, {
canonicalPath: buildCanonicalPath(decoded),
observers: [{ observerId: selectedEntry.observerId }],
network,
});
if (payload.mode === 'resolved' && shouldTouchPredictedOnline(options)) {
await recordPredictedOnline(projection.nodeIds);
}
@@ -692,8 +738,12 @@ export type RegionLink = {
export type MultiObserverResolvedPayload = {
ok: boolean;
packetHash: string;
network: string;
observerCount: number;
sharedPrefixLength: number;
canonicalPath: BetaCanonicalPathNode[];
observers: BetaPathObserver[];
confidence: number | null;
results: BetaResolvedPayload[];
regionLinks?: RegionLink[];
stickyUpdates?: Record<string, string>;
@@ -821,6 +871,7 @@ export async function resolveMultiObserverBetaPath(
const groups = groupCompatibleObservations(entries);
const hiddenCoordMask = buildHiddenCoordMask(context.nodesById);
const results: BetaResolvedPayload[] = [];
let firstCanonicalPath: BetaCanonicalPathNode[] = [];
const predictedOnlineNodeIds = new Set<string>();
const stickyUpdates: Record<string, string> = {};
const stickyConflicts = new Set<string>();
@@ -829,6 +880,7 @@ export async function resolveMultiObserverBetaPath(
for (const group of groups) {
throwIfResolutionAborted(options);
const decoded = decodeBetaCanonicalGroup(group, context, stickyMap, stickyAgeFraction);
if (firstCanonicalPath.length === 0) firstCanonicalPath = buildCanonicalPath(decoded);
for (let position = 0; position < decoded.canonicalHashes.length; position++) {
const hop = decoded.hops.get(position);
const confidence = decoded.hopConfidences.get(position) ?? 0;
@@ -842,7 +894,11 @@ export async function resolveMultiObserverBetaPath(
? context.nodesById.get(entry.packet.src_node_id) ?? null
: null;
const projection = projectCanonicalPathForObserver(entry, decoded, context, source);
const payload = payloadFromProjection(packetHash, entry, projection);
const payload = payloadFromProjection(packetHash, entry, projection, {
canonicalPath: buildCanonicalPath(decoded),
observers: group.members.map((member) => ({ observerId: member.observerId })),
network,
});
results.push(maskResolvedPayload(payload, hiddenCoordMask));
if (payload.mode === 'resolved') {
for (const nodeId of projection.nodeIds) predictedOnlineNodeIds.add(nodeId);
@@ -865,8 +921,12 @@ export async function resolveMultiObserverBetaPath(
return {
ok: true,
packetHash,
network,
observerCount: entries.length,
sharedPrefixLength,
canonicalPath: firstCanonicalPath,
observers: entries.map((entry) => ({ observerId: entry.observerId })),
confidence: results[0]?.confidence ?? null,
results,
...(regionLinks.length > 0 ? { regionLinks } : {}),
...(Object.keys(stickyUpdates).length > 0 ? { stickyUpdates } : {}),
+64
View File
@@ -9,11 +9,27 @@ export type PublicPathExplanation = {
limitations?: string[];
};
export type PublicCanonicalPathNode = {
position: number;
hash: string;
nodeId: string | null;
name: string | null;
lat: number | null;
lon: number | null;
ambiguous: boolean;
confidence: number | null;
};
export type PublicPathObserver = { observerId: string };
export type PublicBetaResultDto = {
ok: boolean;
packetHash: string;
mode: 'resolved' | 'fallback' | 'none';
confidence: number | null;
canonicalPath: PublicCanonicalPathNode[];
observers: PublicPathObserver[];
network: string;
permutationCount: number;
remainingHops: number | null;
purplePath: PublicPathPoint[] | null;
@@ -27,8 +43,12 @@ export type PublicBetaResultDto = {
export type PublicMultiObserverDto = {
ok: boolean;
packetHash: string;
network: string;
observerCount: number;
sharedPrefixLength: number;
canonicalPath: PublicCanonicalPathNode[];
observers: PublicPathObserver[];
confidence: number | null;
results: PublicBetaResultDto[];
};
@@ -108,6 +128,41 @@ function projectExplanation(value: unknown): PublicPathExplanation | undefined {
};
}
function projectCanonicalPath(value: unknown): PublicCanonicalPathNode[] {
if (!Array.isArray(value)) return [];
const nodes: PublicCanonicalPathNode[] = [];
for (const item of value) {
if (!isPlainRecord(item)) continue;
const hash = ownValue(item, 'hash');
const nodeId = ownValue(item, 'nodeId');
const name = ownValue(item, 'name');
if (typeof hash !== 'string') continue;
nodes.push({
position: finiteNumber(ownValue(item, 'position'), -1),
hash,
nodeId: typeof nodeId === 'string' ? nodeId : null,
name: typeof name === 'string' ? name : null,
lat: nullableNumber(ownValue(item, 'lat')),
lon: nullableNumber(ownValue(item, 'lon')),
ambiguous: ownValue(item, 'ambiguous') === true,
confidence: nullableNumber(ownValue(item, 'confidence')),
});
}
return nodes;
}
function projectObservers(value: unknown): PublicPathObserver[] {
if (!Array.isArray(value)) return [];
const observers: PublicPathObserver[] = [];
for (const item of value) {
if (isPlainRecord(item)) {
const observerId = ownValue(item, 'observerId');
if (typeof observerId === 'string') observers.push({ observerId });
}
}
return observers;
}
export function toPublicBetaResultDto(value: unknown): PublicBetaResultDto {
if (!isPlainRecord(value)) throw new Error('INVALID_PATH_RESULT');
const packetHash = ownValue(value, 'packetHash');
@@ -117,11 +172,15 @@ export function toPublicBetaResultDto(value: unknown): PublicBetaResultDto {
throw new Error('INVALID_PATH_RESULT');
}
const explanation = projectExplanation(ownValue(value, 'explanation'));
const network = ownValue(value, 'network');
return {
ok: ownValue(value, 'ok') === true,
packetHash,
mode,
confidence: nullableNumber(ownValue(value, 'confidence')),
canonicalPath: projectCanonicalPath(ownValue(value, 'canonicalPath')),
observers: projectObservers(ownValue(value, 'observers')),
network: typeof network === 'string' ? network : '',
permutationCount: finiteNumber(ownValue(value, 'permutationCount')),
remainingHops: nullableNumber(ownValue(value, 'remainingHops')),
purplePath: projectPath(ownValue(value, 'purplePath')),
@@ -138,11 +197,16 @@ export function toPublicMultiObserverDto(value: unknown): PublicMultiObserverDto
const packetHash = ownValue(value, 'packetHash');
const results = ownValue(value, 'results');
if (typeof packetHash !== 'string' || !Array.isArray(results)) throw new Error('INVALID_PATH_RESULT');
const network = ownValue(value, 'network');
return {
ok: ownValue(value, 'ok') === true,
packetHash,
network: typeof network === 'string' ? network : '',
observerCount: finiteNumber(ownValue(value, 'observerCount')),
sharedPrefixLength: finiteNumber(ownValue(value, 'sharedPrefixLength')),
canonicalPath: projectCanonicalPath(ownValue(value, 'canonicalPath')),
observers: projectObservers(ownValue(value, 'observers')),
confidence: nullableNumber(ownValue(value, 'confidence')),
results: results.map(toPublicBetaResultDto),
};
}