@runnerpro/backend 1.34.0 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Algunas ingestas entregan la distancia del RESUMEN en millas pero rotulada en metros
|
|
3
|
+
* (9.051 m reales -> se guarda 5.600, que son 5,6 millas x 1000). El gráfico y los
|
|
4
|
+
* parciales salen bien porque se calculan del stream, pero la distancia total y el ritmo
|
|
5
|
+
* medio quedan ~1,61x peores, y `getZone` clasifica mal porque sale de duración/distancia.
|
|
6
|
+
*
|
|
7
|
+
* El stream SÍ viene en metros, así que hace de árbitro: se corrige sólo si reconstruir
|
|
8
|
+
* "millas x 1000" desde el stream reproduce la distancia candidata con menos de un 1% de
|
|
9
|
+
* desvío.
|
|
10
|
+
*
|
|
11
|
+
* OJO con leer el filtro de ratio de más abajo como un segundo criterio independiente:
|
|
12
|
+
* NO lo es, es la misma cuenta despejada. Sirve para descartar barato el 99,99% de las
|
|
13
|
+
* ingestas sin dividir. El rango que decide de verdad es ratio ∈ [1,5932 – 1,6254].
|
|
14
|
+
*
|
|
15
|
+
* Ese pasillo es estrecho a propósito, y por eso la corrección deja traza: en producción
|
|
16
|
+
* hay entrenos SANOS pegados a los dos bordes —w699672 a ratio 1,5784 y w694890 a 1,6301,
|
|
17
|
+
* con el stream más largo que el resumen por pausas o deriva de GPS—, y entre 1,40 y 1,80
|
|
18
|
+
* hay una nube continua de entrenos legítimos. Si alguno acaba cayendo dentro, el rastro
|
|
19
|
+
* es lo único que permitirá distinguirlo de un fallo de la fuente.
|
|
20
|
+
*/
|
|
21
|
+
declare const corregirDistanciaEnMillas: (distance: any, streamFinal: number | null) => any;
|
|
1
22
|
/**
|
|
2
23
|
* @param opts {{
|
|
3
24
|
* date, type, title, description, distance, duration, power, desnivel, polyline,
|
|
@@ -10,5 +31,5 @@
|
|
|
10
31
|
* @returns {Promise<number|null>} idWorkout (o null si no se pudo crear) — firma intacta
|
|
11
32
|
*/
|
|
12
33
|
declare const saveWorkoutAplication: (opts: any, _data?: any) => Promise<number | null>;
|
|
13
|
-
export { saveWorkoutAplication };
|
|
34
|
+
export { saveWorkoutAplication, corregirDistanciaEnMillas };
|
|
14
35
|
//# sourceMappingURL=saveWorkoutAplication.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"AA2RA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,QAAA,MAAM,yBAAyB,aAAc,GAAG,eAAe,MAAM,GAAG,IAAI,KAAG,GAe9E,CAAC;AA8FF;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAqFvB,CAAC;AAuGF,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,CAAC"}
|
|
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.saveWorkoutAplication = void 0;
|
|
15
|
+
exports.corregirDistanciaEnMillas = exports.saveWorkoutAplication = void 0;
|
|
16
16
|
const db_1 = require("../db");
|
|
17
17
|
const almacenamiento_1 = require("../almacenamiento");
|
|
18
18
|
const axios_1 = __importDefault(require("axios"));
|
|
@@ -243,6 +243,95 @@ const registrarFuente = (q, idWorkout, tipo, idApp) => __awaiter(void 0, void 0,
|
|
|
243
243
|
ON CONFLICT ("ID WORKOUT", "TIPO APLICATION")
|
|
244
244
|
DO UPDATE SET "ID APLICATION" = EXCLUDED."ID APLICATION", "INGESTED AT" = NOW()`, [idWorkout, String(tipo), idApp !== null && idApp !== undefined ? String(idApp) : null]);
|
|
245
245
|
});
|
|
246
|
+
// =============================================================================
|
|
247
|
+
// Guardarraíl de unidad: distancia en MILLAS rotulada como metros
|
|
248
|
+
// =============================================================================
|
|
249
|
+
const METROS_POR_MILLA = 1609.344;
|
|
250
|
+
// Desvío máximo admitido al reconstruir la candidata desde el stream. NO se puede
|
|
251
|
+
// apretar: Strava redondea la distancia del resumen a una décima de milla, y en un
|
|
252
|
+
// entreno corto ese redondeo ya vale casi un 1% (0,05 mi sobre 3,9 mi = 1,3%).
|
|
253
|
+
const TOLERANCIA_RECONSTRUCCION = 0.01;
|
|
254
|
+
/**
|
|
255
|
+
* Último valor útil del stream de distancia, en metros. Mira primero el stream que
|
|
256
|
+
* entra con esta ingesta y, si no viene, el que ya esté guardado para el workout
|
|
257
|
+
* (p.ej. lo dejó otra fuente). Devuelve null si no hay ninguno: sin árbitro no se
|
|
258
|
+
* toca la distancia.
|
|
259
|
+
*
|
|
260
|
+
* La consulta pide SÓLO el último punto (`-> -1`) y no el array entero: un stream de
|
|
261
|
+
* distancia son miles de puntos en un campo TOAST, y esto corre dentro de la tx que
|
|
262
|
+
* sostiene el advisory lock de la ingesta. Traerlo completo para leer un número sería
|
|
263
|
+
* alargar el lock que ya es el punto donde más se cae el pool.
|
|
264
|
+
*/
|
|
265
|
+
const distanciaFinalDelStream = (q, idWorkout, streams) => __awaiter(void 0, void 0, void 0, function* () {
|
|
266
|
+
const valido = (v) => {
|
|
267
|
+
const n = Number(v);
|
|
268
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
269
|
+
};
|
|
270
|
+
const entrantes = streams === null || streams === void 0 ? void 0 : streams.distance;
|
|
271
|
+
if (Array.isArray(entrantes) && entrantes.length > 0) {
|
|
272
|
+
const ultimo = valido(entrantes[entrantes.length - 1]);
|
|
273
|
+
if (ultimo !== null)
|
|
274
|
+
return ultimo;
|
|
275
|
+
}
|
|
276
|
+
const [row] = yield q(`SELECT CASE WHEN jsonb_typeof("DATA"::jsonb) = 'array' AND jsonb_array_length("DATA"::jsonb) > 0
|
|
277
|
+
THEN "DATA"::jsonb -> -1 ELSE NULL END AS ultimo
|
|
278
|
+
FROM "WORKOUT STREAM" WHERE "ID WORKOUT" = ? AND "METRIC TYPE" = ?`, [idWorkout, 'distance']);
|
|
279
|
+
return row ? valido(row.ultimo) : null;
|
|
280
|
+
});
|
|
281
|
+
/**
|
|
282
|
+
* Algunas ingestas entregan la distancia del RESUMEN en millas pero rotulada en metros
|
|
283
|
+
* (9.051 m reales -> se guarda 5.600, que son 5,6 millas x 1000). El gráfico y los
|
|
284
|
+
* parciales salen bien porque se calculan del stream, pero la distancia total y el ritmo
|
|
285
|
+
* medio quedan ~1,61x peores, y `getZone` clasifica mal porque sale de duración/distancia.
|
|
286
|
+
*
|
|
287
|
+
* El stream SÍ viene en metros, así que hace de árbitro: se corrige sólo si reconstruir
|
|
288
|
+
* "millas x 1000" desde el stream reproduce la distancia candidata con menos de un 1% de
|
|
289
|
+
* desvío.
|
|
290
|
+
*
|
|
291
|
+
* OJO con leer el filtro de ratio de más abajo como un segundo criterio independiente:
|
|
292
|
+
* NO lo es, es la misma cuenta despejada. Sirve para descartar barato el 99,99% de las
|
|
293
|
+
* ingestas sin dividir. El rango que decide de verdad es ratio ∈ [1,5932 – 1,6254].
|
|
294
|
+
*
|
|
295
|
+
* Ese pasillo es estrecho a propósito, y por eso la corrección deja traza: en producción
|
|
296
|
+
* hay entrenos SANOS pegados a los dos bordes —w699672 a ratio 1,5784 y w694890 a 1,6301,
|
|
297
|
+
* con el stream más largo que el resumen por pausas o deriva de GPS—, y entre 1,40 y 1,80
|
|
298
|
+
* hay una nube continua de entrenos legítimos. Si alguno acaba cayendo dentro, el rastro
|
|
299
|
+
* es lo único que permitirá distinguirlo de un fallo de la fuente.
|
|
300
|
+
*/
|
|
301
|
+
const corregirDistanciaEnMillas = (distance, streamFinal) => {
|
|
302
|
+
const d = Number(distance);
|
|
303
|
+
if (!Number.isFinite(d) || d <= 0 || streamFinal === null || !(streamFinal > 0))
|
|
304
|
+
return distance;
|
|
305
|
+
// Filtro barato: fuera de la horquilla de la milla no hay nada que mirar.
|
|
306
|
+
const ratio = streamFinal / d;
|
|
307
|
+
if (ratio < 1.58 || ratio > 1.64)
|
|
308
|
+
return distance;
|
|
309
|
+
// La fuente trató las MILLAS como si fueran kilómetros: el valor que nos llega es
|
|
310
|
+
// "millas x 1000" (5,6 mi -> 5600). Reconstruimos ese número desde el stream y exigimos
|
|
311
|
+
// que reproduzca la candidata; si no, el desajuste tiene otra causa y no se toca.
|
|
312
|
+
const millasEscaladas = (streamFinal / METROS_POR_MILLA) * 1000;
|
|
313
|
+
if (Math.abs(millasEscaladas - d) / d > TOLERANCIA_RECONSTRUCCION)
|
|
314
|
+
return distance;
|
|
315
|
+
return Math.round(streamFinal / 10) * 10;
|
|
316
|
+
};
|
|
317
|
+
exports.corregirDistanciaEnMillas = corregirDistanciaEnMillas;
|
|
318
|
+
const resolverDistanciaConStream = (q, idWorkout, distance, streams) => __awaiter(void 0, void 0, void 0, function* () {
|
|
319
|
+
const d = Number(distance);
|
|
320
|
+
if (!Number.isFinite(d) || d <= 0)
|
|
321
|
+
return distance;
|
|
322
|
+
const streamFinal = yield distanciaFinalDelStream(q, idWorkout, streams);
|
|
323
|
+
const corregida = corregirDistanciaEnMillas(distance, streamFinal);
|
|
324
|
+
// Traza obligatoria: esto pisa un dato del cliente por heurística y en un margen fino.
|
|
325
|
+
// Sin rastro, un "mi entreno marca de más" no se podría distinguir de un fallo de la
|
|
326
|
+
// fuente, ni se sabría cuántas veces ha actuado el guardarraíl.
|
|
327
|
+
if (Number(corregida) !== d) {
|
|
328
|
+
// eslint-disable-next-line no-console
|
|
329
|
+
console.warn(`corregirDistanciaEnMillas: idWorkout=${idWorkout} DISTANCE ${d} -> ${corregida} ` +
|
|
330
|
+
`(stream=${streamFinal}, ratio=${(Number(streamFinal) / d).toFixed(4)}). ` +
|
|
331
|
+
'La fuente entregó la distancia del resumen en millas.');
|
|
332
|
+
}
|
|
333
|
+
return corregida;
|
|
334
|
+
});
|
|
246
335
|
const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void 0, void 0, function* () {
|
|
247
336
|
var _a, _b, _c, _d;
|
|
248
337
|
const [row] = yield q(`SELECT "DISTANCE", "DURATION", "POTENCIA", "DESNIVEL", "CALORIES", "FC MEDIA",
|
|
@@ -250,7 +339,10 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
|
|
|
250
339
|
FROM "WORKOUT" WHERE "ID" = ? FOR UPDATE`, [idWorkout]);
|
|
251
340
|
const prioNueva = prioridadFuente(opts.aplicationType);
|
|
252
341
|
const prioActual = esFilaNueva ? -1 : prioridadFuente(row === null || row === void 0 ? void 0 : row.tipoAplication);
|
|
253
|
-
const
|
|
342
|
+
const distanceMerge = resolverEscalar(row === null || row === void 0 ? void 0 : row.distance, opts.distance, prioActual, prioNueva);
|
|
343
|
+
// El stream (metros) arbitra sobre la distancia del resumen: corrige el caso en que la
|
|
344
|
+
// fuente la entrega en millas. Va ANTES de getZone, que se calcula con esta distancia.
|
|
345
|
+
const distance = yield resolverDistanciaConStream(q, idWorkout, distanceMerge, opts.streams);
|
|
254
346
|
const duration = resolverEscalar(row === null || row === void 0 ? void 0 : row.duration, opts.duration, prioActual, prioNueva);
|
|
255
347
|
const potencia = resolverEscalar(row === null || row === void 0 ? void 0 : row.potencia, opts.power, prioActual, prioNueva);
|
|
256
348
|
const desnivel = resolverEscalar(row === null || row === void 0 ? void 0 : row.desnivel, opts.desnivel, prioActual, prioNueva);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runnerpro/backend",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.1",
|
|
4
4
|
"description": "A collection of common backend functions",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./lib/cjs/index.js"
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"test:generate-share-map-guard": "ts-node --transpile-only -P configs/tsconfig.cjs.json scripts/testGenerateShareMapGuard.ts",
|
|
21
21
|
"test:pg-query-conexion": "ts-node --transpile-only -P configs/tsconfig.cjs.json scripts/testPgQueryConexion.ts",
|
|
22
22
|
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
|
23
|
-
"prepare": "husky"
|
|
23
|
+
"prepare": "husky",
|
|
24
|
+
"test:distancia-millas": "ts-node --transpile-only -P configs/tsconfig.cjs.json scripts/testDistanciaMillas.ts"
|
|
24
25
|
},
|
|
25
26
|
"release": {
|
|
26
27
|
"branches": [
|