@open3cl/engine 1.7.1 → 1.7.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -42,11 +42,8 @@
42
42
  "dependencies": {
43
43
  "dioma": "^0.4.6",
44
44
  "fast-xml-parser": "^5.11.1",
45
- "http-server": "^14.1.1",
46
45
  "lodash-es": "^4.18.1",
47
- "mathjs": "^15.2.0",
48
- "winston": "^3.19.0",
49
- "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
46
+ "winston": "^3.19.0"
50
47
  },
51
48
  "devDependencies": {
52
49
  "@aws-sdk/client-s3": "^3.1013.0",
@@ -71,13 +68,15 @@
71
68
  "eslint-plugin-n": "^16.6.2",
72
69
  "eslint-plugin-promise": "^6.1.1",
73
70
  "fast-csv": "^5.0.2",
71
+ "http-server": "^14.1.1",
74
72
  "husky": "^9.0.11",
75
73
  "jscpd": "^3.5.10",
76
74
  "piscina": "^5.1.2",
77
75
  "prettier": "^3.2.5",
78
76
  "pretty-quick": "^4.0.0",
79
77
  "semantic-release": "^25.0.2",
80
- "vitest": "3.2.7"
78
+ "vitest": "3.2.7",
79
+ "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
81
80
  },
82
81
  "author": "Open3Cl",
83
82
  "license": "GPL-3.0",
package/utils.js CHANGED
@@ -2,7 +2,7 @@ import enums from './enums.js';
2
2
  import tvs from './tv.js';
3
3
  import { set } from 'lodash-es';
4
4
  import { XMLParser } from 'fast-xml-parser';
5
- import { evaluate } from 'mathjs';
5
+ import { evaluateFormula } from './formula.js';
6
6
 
7
7
  export const xmlParser = new XMLParser({
8
8
  // We want to make sure collections of length 1 are still parsed as arrays
@@ -57,13 +57,99 @@ export function set_use_enum_as_string() {
57
57
  use_enum_as_string = true;
58
58
  }
59
59
 
60
+ /**
61
+ * Implémentation de `tvMatch` réellement utilisée.
62
+ *
63
+ * Le choix est fait une seule fois, au moment du réglage, et non à chaque appel :
64
+ * `tv_match_new_version` est un *live binding* ESM, donc un `if (tv_match_new_version)`
65
+ * dans `tvMatch` force V8 à relire le slot de module à chaque appel et l'empêche
66
+ * d'inliner la délégation. Sur plusieurs millions d'appels par calcul, ce seul
67
+ * aiguillage représentait ~10% du temps CPU total.
68
+ *
69
+ * Les déclarations de fonction étant hoistées, la référence est valide ici.
70
+ */
71
+ let tvMatchImpl = tvMatchLegacy;
72
+
73
+ /**
74
+ * Cache mémoïsant `tv()`.
75
+ *
76
+ * Les tables de valeurs sont statiques et `tv()` est pure : pour un même couple
77
+ * (filePath, matcher) la ligne retournée est toujours la même. Le moteur rejoue
78
+ * les mêmes combinaisons des milliers de fois par DPE.
79
+ *
80
+ * Éviction FIFO : certains matchers contiennent des valeurs issues des données
81
+ * d'entrée (ug, aiu/aue…), la cardinalité n'est donc pas fermée sur un process
82
+ * qui tourne longtemps.
83
+ */
84
+ let TV_CACHE_MAX = 20000;
85
+
86
+ /**
87
+ * Change la borne du cache de `tv()`.
88
+ *
89
+ * `Infinity` désactive l'éviction — utile pour les outils de diagnostic, qui ont besoin de
90
+ * l'union réelle des matchers et non d'une taille plafonnée. Réduire la borne n'évince pas
91
+ * immédiatement : les entrées en trop partent au fil des insertions suivantes.
92
+ *
93
+ * @param max {number}
94
+ */
95
+ export function setTvCacheMax(max) {
96
+ TV_CACHE_MAX = max;
97
+ }
98
+
99
+ /** @returns {number} borne courante du cache de `tv()`. */
100
+ export function getTvCacheMax() {
101
+ return TV_CACHE_MAX;
102
+ }
103
+ const tvCache = new Map();
104
+
105
+ /**
106
+ * Vide le cache de `tv()`.
107
+ * À appeler si les tables de valeurs ou la sémantique de matching changent.
108
+ */
109
+ export function clearTvCache() {
110
+ tvCache.clear();
111
+ }
112
+
113
+ /**
114
+ * Nombre d'entrées actuellement mémoïsées, c'est-à-dire de matchers distincts rencontrés depuis
115
+ * le dernier `clearTvCache()`. Purement diagnostique (bancs de mesure, métriques) : aucun
116
+ * compteur n'est tenu sur le chemin chaud, la taille est lue à la demande.
117
+ *
118
+ * @returns {number}
119
+ */
120
+ export function tvCacheSize() {
121
+ return tvCache.size;
122
+ }
123
+
124
+ /**
125
+ * Clés actuellement mémoïsées, au format interne : `table` puis, pour chaque entrée du matcher,
126
+ * `clévaleur`. Purement diagnostique — sert à analyser quelle table et quelle colonne
127
+ * génèrent de la cardinalité ouverte. Ne pas utiliser en production : la liste est copiée.
128
+ *
129
+ * @returns {string[]}
130
+ */
131
+ export function tvCacheKeys() {
132
+ return [...tvCache.keys()];
133
+ }
134
+
135
+ /**
136
+ * Séparateurs utilisés dans les clés du cache, exposés pour que les outils de diagnostic
137
+ * n'aient pas à les redéclarer.
138
+ */
139
+ export const TV_CACHE_KEY_SEPARATORS = { entry: '', value: '' };
140
+
60
141
  export let tv_match_new_version = false;
61
142
  export function set_tv_match_optimized_version() {
62
143
  tv_match_new_version = true;
144
+ tvMatchImpl = tvMatchOptimized;
145
+ // La sémantique de matching change : les résultats mémoïsés ne sont plus valides.
146
+ clearTvCache();
63
147
  }
64
148
 
65
149
  export function unset_tv_match_optimized_version() {
66
150
  tv_match_new_version = false;
151
+ tvMatchImpl = tvMatchLegacy;
152
+ clearTvCache();
67
153
  }
68
154
 
69
155
  export const Tbase = {
@@ -156,11 +242,29 @@ export function add_references(enveloppe) {
156
242
  }
157
243
  }
158
244
 
245
+ /**
246
+ * `Object.keys(enums[field])` mémoïsé.
247
+ *
248
+ * ATTENTION : le tableau retourné est partagé entre tous les appels et affecté tel
249
+ * quel à `du[enum_*_id]`. C'est sans risque tant que personne ne mute ces tableaux
250
+ * (aucun appelant ne le fait aujourd'hui) ; si un jour c'est le cas, remplacer par
251
+ * `enumKeys(field).slice()`.
252
+ */
253
+ const enumKeysCache = new Map();
254
+ function enumKeys(field) {
255
+ let keys = enumKeysCache.get(field);
256
+ if (keys === undefined) {
257
+ keys = Object.keys(enums[field]);
258
+ enumKeysCache.set(field, keys);
259
+ }
260
+ return keys;
261
+ }
262
+
159
263
  export function requestInputID(de, du, field, type) {
160
264
  // enums
161
265
  const enum_name = `enum_${field}_id`;
162
266
  if (type) du[enum_name] = type;
163
- else du[enum_name] = Object.keys(enums[field]);
267
+ else du[enum_name] = enumKeys(field);
164
268
  return de[enum_name];
165
269
  }
166
270
 
@@ -169,7 +273,7 @@ export function requestInput(de, du, field, type) {
169
273
  // enums
170
274
  const enum_name = `enum_${field}_id`;
171
275
  if (type) du[enum_name] = type;
172
- else du[enum_name] = Object.keys(enums[field]);
276
+ else du[enum_name] = enumKeys(field);
173
277
  return enums[field][de[enum_name]];
174
278
  } else {
175
279
  // not enums
@@ -186,21 +290,38 @@ export function getKeyByValue(object, value) {
186
290
  return Object.keys(object).find((key) => object[key] === value);
187
291
  }
188
292
 
293
+ /**
294
+ * `tvColumnIDs` est une fonction pure de deux constantes (une table statique et un
295
+ * nom de colonne), mais elle était rappelée pour chaque générateur de chaque DPE.
296
+ * Le `unique_ids.includes(value)` en O(n²) est remplacé par un `Set`.
297
+ *
298
+ * Le tableau retourné est partagé : les appelants ne font que le lire (`includes`).
299
+ */
300
+ const tvColumnIDsCache = new Map();
301
+
189
302
  export function tvColumnIDs(filePath, field) {
303
+ const cacheKey = `${filePath}${field}`;
304
+ const cached = tvColumnIDsCache.get(cacheKey);
305
+ if (cached !== undefined) return cached;
306
+
190
307
  const list = tvs[filePath];
191
308
  const enum_name = `enum_${field}_id`;
192
- let ids = list.map((row) => row[enum_name]);
193
- // remove undefineds
194
- ids = ids.filter((id) => id);
195
309
  // split each by | and get uniques
310
+ const seen = new Set();
196
311
  const unique_ids = [];
197
- for (const id of ids) {
198
- const values = id.split('|');
199
- for (const value of values) {
200
- if (!unique_ids.includes(value)) unique_ids.push(value);
312
+ for (const row of list) {
313
+ const id = row[enum_name];
314
+ // remove undefineds
315
+ if (!id) continue;
316
+ for (const value of id.split('|')) {
317
+ if (!seen.has(value)) {
318
+ seen.add(value);
319
+ unique_ids.push(value);
320
+ }
201
321
  }
202
322
  }
203
- // sort like numbers
323
+
324
+ tvColumnIDsCache.set(cacheKey, unique_ids);
204
325
  return unique_ids;
205
326
  }
206
327
 
@@ -211,7 +332,7 @@ export function tvColumnLines(filePath, column, matcher) {
211
332
  for (const row of list) {
212
333
  let match = true;
213
334
  for (const key in matcher) {
214
- if (tvMatch(row, key, matcher) === false) {
335
+ if (tvMatchImpl(row, key, matcher) === false) {
215
336
  match = false;
216
337
  break;
217
338
  }
@@ -221,10 +342,7 @@ export function tvColumnLines(filePath, column, matcher) {
221
342
  return lines;
222
343
  }
223
344
 
224
- function tvMatch(row, key, matcher) {
225
- if (tv_match_new_version) {
226
- return tvMatchOptimized(row, key, matcher);
227
- }
345
+ function tvMatchLegacy(row, key, matcher) {
228
346
  if (!row.hasOwnProperty(key)) {
229
347
  // for empty csv columns
230
348
  // for q4pa_conv
@@ -291,7 +409,28 @@ function tvMatchOptimized(row, key, matcher) {
291
409
  }
292
410
 
293
411
  export function tv(filePath, matcher) {
412
+ let cacheKey = filePath;
413
+ for (const key in matcher) cacheKey += `${key}${matcher[key]}`;
414
+
415
+ const cached = tvCache.get(cacheKey);
416
+ // `tvLookup` ne retourne jamais `undefined` (une ligne ou `null`) : `undefined`
417
+ // distingue donc bien un défaut de cache d'un `null` mémoïsé.
418
+ if (cached !== undefined) return cached;
419
+
420
+ const result = tvLookup(filePath, matcher);
421
+
422
+ if (tvCache.size >= TV_CACHE_MAX) {
423
+ // Éviction FIFO : `Map` conserve l'ordre d'insertion.
424
+ tvCache.delete(tvCache.keys().next().value);
425
+ }
426
+ tvCache.set(cacheKey, result);
427
+
428
+ return result;
429
+ }
430
+
431
+ function tvLookup(filePath, matcher) {
294
432
  const list = tvs[filePath];
433
+ const matcher_size = Object.keys(matcher).length;
295
434
  let match_count = 0;
296
435
  let max_match_count = 0;
297
436
  let match = null;
@@ -299,10 +438,10 @@ export function tv(filePath, matcher) {
299
438
  for (const row of list) {
300
439
  match_count = 0;
301
440
  for (const key in matcher) {
302
- if (tvMatch(row, key, matcher)) match_count += 1;
441
+ if (tvMatchImpl(row, key, matcher)) match_count += 1;
303
442
  }
304
443
  // if match_count is same as matcher, we are done
305
- if (match_count === Object.keys(matcher).length) return row;
444
+ if (match_count === matcher_size) return row;
306
445
 
307
446
  /* if (filePath === 'q4pa_conv') console.warn(match_count) */
308
447
  if (match_count > max_match_count) {
@@ -385,22 +524,51 @@ export function getVolumeStockageFromDescription(description) {
385
524
  return matching && matching.length > 1 ? parseInt(matching[1], 10) : 0;
386
525
  }
387
526
 
527
+ /**
528
+ * Cache de `cleanReference`.
529
+ *
530
+ * Les r\u00e9f\u00e9rences proviennent des DPE en entr\u00e9e, leur cardinalit\u00e9 n'est donc pas ferm\u00e9e sur un
531
+ * process qui tourne longtemps (\u00e9coute NATS, worker) : le cache est born\u00e9 et \u00e9vinc\u00e9 en FIFO,
532
+ * comme celui de `tv()`.
533
+ */
534
+ const CLEAN_REFERENCE_CACHE_MAX = 5000;
535
+ const cleanReferenceCache = new Map();
536
+
388
537
  /**
389
538
  * Remove space and accented characters
539
+ *
540
+ * M\u00e9mo\u00efs\u00e9e : `normalize('NFD')` suivi de deux remplacements par expression r\u00e9guli\u00e8re co\u00fbte cher,
541
+ * et les m\u00eames r\u00e9f\u00e9rences sont compar\u00e9es en boucle (chaque pont thermique est confront\u00e9 \u00e0 toutes
542
+ * les parois).
543
+ *
390
544
  * @param reference {string}
391
545
  * return {string}
392
546
  */
393
547
  export function cleanReference(reference) {
394
- if (reference) {
395
- return reference
396
- .toString()
397
- .toLowerCase()
398
- .normalize('NFD')
399
- .replace(/[\u0300-\u036f]/g, '')
400
- .replace(/\s+/g, '');
548
+ // Valeurs falsy (undefined, null, '', 0) : retourn\u00e9es telles quelles, sans passer par le cache.
549
+ if (!reference) {
550
+ return reference;
551
+ }
552
+
553
+ const cached = cleanReferenceCache.get(reference);
554
+ if (cached !== undefined) {
555
+ return cached;
556
+ }
557
+
558
+ const cleaned = reference
559
+ .toString()
560
+ .toLowerCase()
561
+ .normalize('NFD')
562
+ .replace(/[\u0300-\u036f]/g, '')
563
+ .replace(/\s+/g, '');
564
+
565
+ if (cleanReferenceCache.size >= CLEAN_REFERENCE_CACHE_MAX) {
566
+ // \u00c9viction FIFO : `Map` conserve l'ordre d'insertion.
567
+ cleanReferenceCache.delete(cleanReferenceCache.keys().next().value);
401
568
  }
569
+ cleanReferenceCache.set(reference, cleaned);
402
570
 
403
- return reference;
571
+ return cleaned;
404
572
  }
405
573
 
406
574
  /**
@@ -492,24 +660,7 @@ export function containsAnySubstring(mainString, substrings) {
492
660
  * @returns {number} Résultat de l'évaluation.
493
661
  */
494
662
  export function excel_to_js_exec(formulaOrValue, pn, E, F) {
495
- // Les formules des tables utilisent la virgule comme séparateur décimal (ex. `0,085`),
496
- // à convertir en point pour être évaluées.
497
- const formula =
498
- typeof formulaOrValue === 'string'
499
- ? formulaOrValue.replace(/(\d),(\d)/g, '$1.$2')
500
- : formulaOrValue;
501
-
502
- if (!isNaN(formula)) {
503
- return Number(formula);
504
- }
505
-
506
- const Pn = pn / 1000;
507
- return evaluate(formula, {
508
- Pn,
509
- logPn: Math.log10(Pn),
510
- E,
511
- F
512
- });
663
+ return evaluateFormula(formulaOrValue, pn, E, F);
513
664
  }
514
665
 
515
666
  /**
package/utils.spec.js CHANGED
@@ -41,6 +41,12 @@ const {
41
41
  tvColumnIDs,
42
42
  tvColumnLines,
43
43
  tv,
44
+ clearTvCache,
45
+ setTvCacheMax,
46
+ getTvCacheMax,
47
+ tvCacheSize,
48
+ tvCacheKeys,
49
+ TV_CACHE_KEY_SEPARATORS,
44
50
  removeKeyFromJSON,
45
51
  clean_dpe,
46
52
  getThicknessFromDescription,
@@ -308,6 +314,11 @@ describe('Utils unit tests', () => {
308
314
  test('retourne les identifiants uniques éclatés', () => {
309
315
  expect(tvColumnIDs('colonnes', 'x')).toStrictEqual(['1', '2', '3']);
310
316
  });
317
+
318
+ test('met en cache le résultat : le même tableau est retourné au second appel', () => {
319
+ const first = tvColumnIDs('colonnes', 'x');
320
+ expect(tvColumnIDs('colonnes', 'x')).toBe(first);
321
+ });
311
322
  });
312
323
 
313
324
  // -------------------------------------------------------------------------------------------
@@ -420,6 +431,53 @@ describe('Utils unit tests', () => {
420
431
  });
421
432
  });
422
433
 
434
+ // -------------------------------------------------------------------------------------------
435
+ describe('tv / cache', () => {
436
+ // Règle : `tv()` mémoïse chaque couple (table, matcher), borné par une éviction FIFO.
437
+ let initialMax;
438
+
439
+ beforeEach(() => {
440
+ initialMax = getTvCacheMax();
441
+ clearTvCache();
442
+ return () => {
443
+ setTvCacheMax(initialMax);
444
+ clearTvCache();
445
+ };
446
+ });
447
+
448
+ test('mémoïse le résultat, y compris un résultat null', () => {
449
+ const row = tv('table_defaut', { enum_x_id: '1' });
450
+ expect(tv('table_defaut', { enum_x_id: '1' })).toBe(row);
451
+ expect(tv('table_defaut', { enum_x_id: 'absent' })).toBeNull();
452
+ expect(tv('table_defaut', { enum_x_id: 'absent' })).toBeNull();
453
+ expect(tvCacheSize()).toBe(2);
454
+ });
455
+
456
+ test('expose les clés du cache au format table + clé + valeur', () => {
457
+ tv('table_defaut', { enum_x_id: '1' });
458
+ const { entry, value } = TV_CACHE_KEY_SEPARATORS;
459
+ expect(tvCacheKeys()).toStrictEqual([`table_defaut${entry}enum_x_id${value}1`]);
460
+ });
461
+
462
+ test('évince la plus ancienne entrée au-delà de la borne', () => {
463
+ setTvCacheMax(2);
464
+ expect(getTvCacheMax()).toBe(2);
465
+
466
+ tv('table_defaut', { enum_x_id: '1' });
467
+ tv('table_defaut', { enum_x_id: '2' });
468
+ tv('table_defaut', { enum_x_id: '3' });
469
+
470
+ expect(tvCacheSize()).toBe(2);
471
+ expect(tvCacheKeys().some((key) => key.endsWith('1'))).toBe(false);
472
+ });
473
+
474
+ test('clearTvCache vide le cache', () => {
475
+ tv('table_defaut', { enum_x_id: '1' });
476
+ clearTvCache();
477
+ expect(tvCacheSize()).toBe(0);
478
+ });
479
+ });
480
+
423
481
  // -------------------------------------------------------------------------------------------
424
482
  describe('removeKeyFromJSON', () => {
425
483
  // Règle : supprime récursivement la clé cible, sauf sous les branches listées dans skipKeys.
@@ -513,6 +571,17 @@ describe('Utils unit tests', () => {
513
571
  expect(cleanReference(input)).toBe(expected);
514
572
  });
515
573
 
574
+ test('retourne le résultat mémoïsé au second appel', () => {
575
+ expect(cleanReference('Réf Mémo')).toBe('refmemo');
576
+ expect(cleanReference('Réf Mémo')).toBe('refmemo');
577
+ });
578
+
579
+ test('reste correct une fois la borne du cache dépassée (éviction FIFO)', () => {
580
+ for (let i = 0; i <= 5000; i += 1) cleanReference(`Réf éviction ${i}`);
581
+ expect(cleanReference('Réf éviction 0')).toBe('refeviction0');
582
+ expect(cleanReference('Réf éviction 5000')).toBe('refeviction5000');
583
+ });
584
+
516
585
  test('compareReferences ignore espaces et accents', () => {
517
586
  expect(compareReferences('Réf 1', 'ref1')).toBe(true);
518
587
  expect(compareReferences('a', 'b')).toBe(false);