fix(places): read an imported row for the dedup set in one helper

The candidate a GPX or KML row is checked as is built in lineCandidate, so
places.service.ts stays within its size baseline, and the atomic import test
imports the same Ridge line it later expects to be skipped.
This commit is contained in:
Maurice
2026-10-28 15:17:37 +01:00
parent ef86bf2660
commit aee275e2b0
3 changed files with 16 additions and 7 deletions
+8
View File
@@ -134,6 +134,14 @@ function lineCourse(routeGeometry: string | null | undefined): string {
return [first[0], first[1], last[0], last[1], south, west, north, east].map((v) => v.toFixed(3)).join(',');
}
/** An imported row as the dedup set reads it: a line when it carries one. */
export function lineCandidate(
name: string | null | undefined,
row: { lat: number | null; lng: number | null; routeGeometry?: string | null },
): DedupCandidate {
return { name, lat: row.lat, lng: row.lng, track: !!row.routeGeometry, routeGeometry: row.routeGeometry };
}
/** A track's entry in `DedupSet.tracks`: its normalized name and its course. */
function trackKey(name: string, routeGeometry: string | null | undefined): string {
return `${name}\u0000${lineCourse(routeGeometry)}`;
+6 -6
View File
@@ -58,7 +58,7 @@ import {
buildCategoryNameLookup,
escapeLikePattern,
dedupSetOf,
isPlaceDuplicate,
isPlaceDuplicate, lineCandidate,
mapWithConcurrency,
pickEnrichmentMatch,
reclaimPhotoCache,
@@ -758,7 +758,7 @@ export class PlacesService {
async gpxDuplicates(tripId: string, rows: PreparedGpxPlace[]): Promise<boolean[]> {
const dedup = await this.buildDedupSet(tripId);
return rows.map((wp) => {
const candidate = { name: wp.name, lat: wp.lat, lng: wp.lng, track: !!wp.routeGeometry, routeGeometry: wp.routeGeometry };
const candidate = lineCandidate(wp.name, wp);
if (isPlaceDuplicate(candidate, dedup)) return true;
trackInsertedInDedupSet(candidate, dedup);
return false;
@@ -925,7 +925,7 @@ export class PlacesService {
// `findWithTagsAndRatings` INSIDE the caller's transaction, so the read
// sees the still-uncommitted row.
for (const wp of waypoints) {
if (isPlaceDuplicate({ name: wp.name, lat: wp.lat, lng: wp.lng, track: !!wp.routeGeometry, routeGeometry: wp.routeGeometry }, dedup)) {
if (isPlaceDuplicate(lineCandidate(wp.name, wp), dedup)) {
skipped++;
continue;
}
@@ -960,7 +960,7 @@ export class PlacesService {
});
const place = (await this.placesRepo.findWithTagsAndRatings(placeId))!;
created.push(place);
trackInsertedInDedupSet({ name: wp.name, lat: wp.lat, lng: wp.lng, track: !!wp.routeGeometry, routeGeometry: wp.routeGeometry }, dedup);
trackInsertedInDedupSet(lineCandidate(wp.name, wp), dedup);
}
return { places: created, count: created.length, skipped };
@@ -1033,7 +1033,7 @@ export class PlacesService {
const fallbackName = `Placemark ${fallbackIndex}`;
const name = parsedPlacemark.name || fallbackName;
if (isPlaceDuplicate({ name, lat: parsedPlacemark.lat, lng: parsedPlacemark.lng, track: isPath, routeGeometry: parsedPlacemark.routeGeometry }, dedup)) {
if (isPlaceDuplicate(lineCandidate(name, parsedPlacemark), dedup)) {
summary.skippedCount += 1;
dupCount++;
fallbackIndex += 1;
@@ -1079,7 +1079,7 @@ export class PlacesService {
const place = (await this.placesRepo.findWithTagsAndRatings(placeId))!;
created.push(place);
trackInsertedInDedupSet({ name, lat: parsedPlacemark.lat, lng: parsedPlacemark.lng, track: isPath, routeGeometry: parsedPlacemark.routeGeometry }, dedup);
trackInsertedInDedupSet(lineCandidate(name, parsedPlacemark), dedup);
summary.createdCount += 1;
fallbackIndex += 1;
}
@@ -418,9 +418,10 @@ describe('Tours GPX atomic persistence and postcommit publication', () => {
});
it('imports only the new tracks and publishes only committed rows in a mixed result', async () => {
// The same Ridge as in the mixed file, line and all: a track is a duplicate only of one that runs alike (#2870).
const existing = Buffer.from(`<gpx><trk><name>Ridge</name><trkseg>
<trkpt lat="49" lon="12"><ele>100</ele></trkpt><trkpt lat="49.01" lon="12.01"><ele>150</ele></trkpt>
</trkseg></trk></gpx>`);
</trkseg><trkseg><trkpt lat="49.02" lon="12.02"><ele>120</ele></trkpt></trkseg></trk></gpx>`);
expect((await tours.importGpxAsTour(tripId, existing))?.tours).toHaveLength(1);
broadcast.mockClear();