@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":"AAqTA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAqFvB,CAAC;AAuGF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
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 distance = resolverEscalar(row === null || row === void 0 ? void 0 : row.distance, opts.distance, prioActual, prioNueva);
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.0",
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": [