@futdevpro/nts-dynamo 1.15.99 → 1.15.101

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.
Files changed (66) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1855 -1791
  2. package/.dynamo/logs/cicd-pipeline/status.json +29 -29
  3. package/build/_collections/archive-service-registry.const.d.ts +16 -0
  4. package/build/_collections/archive-service-registry.const.d.ts.map +1 -0
  5. package/build/_collections/archive-service-registry.const.js +15 -0
  6. package/build/_collections/archive-service-registry.const.js.map +1 -0
  7. package/build/_collections/client-safe-error.util.d.ts +33 -0
  8. package/build/_collections/client-safe-error.util.d.ts.map +1 -0
  9. package/build/_collections/client-safe-error.util.js +68 -0
  10. package/build/_collections/client-safe-error.util.js.map +1 -0
  11. package/build/_collections/global-settings.const.d.ts.map +1 -1
  12. package/build/_collections/global-settings.const.js +9 -0
  13. package/build/_collections/global-settings.const.js.map +1 -1
  14. package/build/_collections/load-shed.util.d.ts +16 -0
  15. package/build/_collections/load-shed.util.d.ts.map +1 -0
  16. package/build/_collections/load-shed.util.js +43 -0
  17. package/build/_collections/load-shed.util.js.map +1 -0
  18. package/build/_models/control-models/endpoint-params.control-model.d.ts.map +1 -1
  19. package/build/_models/control-models/endpoint-params.control-model.js +6 -11
  20. package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
  21. package/build/_models/interfaces/global-settings.interface.d.ts +26 -0
  22. package/build/_models/interfaces/global-settings.interface.d.ts.map +1 -1
  23. package/build/_models/interfaces/static-client-settings.interface.d.ts +8 -0
  24. package/build/_models/interfaces/static-client-settings.interface.d.ts.map +1 -1
  25. package/build/_modules/server/server-status/server-status.controller.d.ts.map +1 -1
  26. package/build/_modules/server/server-status/server-status.controller.js +19 -0
  27. package/build/_modules/server/server-status/server-status.controller.js.map +1 -1
  28. package/build/_services/base/archive-data.service.d.ts.map +1 -1
  29. package/build/_services/base/archive-data.service.js +3 -0
  30. package/build/_services/base/archive-data.service.js.map +1 -1
  31. package/build/_services/base/data.service.d.ts.map +1 -1
  32. package/build/_services/base/data.service.js +30 -16
  33. package/build/_services/base/data.service.js.map +1 -1
  34. package/build/_services/base/db.service.d.ts +15 -0
  35. package/build/_services/base/db.service.d.ts.map +1 -1
  36. package/build/_services/base/db.service.js +41 -0
  37. package/build/_services/base/db.service.js.map +1 -1
  38. package/build/_services/core/event-loop-diag.service.d.ts +44 -0
  39. package/build/_services/core/event-loop-diag.service.d.ts.map +1 -0
  40. package/build/_services/core/event-loop-diag.service.js +89 -0
  41. package/build/_services/core/event-loop-diag.service.js.map +1 -0
  42. package/build/_services/server/app.server.d.ts.map +1 -1
  43. package/build/_services/server/app.server.js +44 -1
  44. package/build/_services/server/app.server.js.map +1 -1
  45. package/build/index.d.ts +3 -0
  46. package/build/index.d.ts.map +1 -1
  47. package/build/index.js +3 -0
  48. package/build/index.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/_collections/archive-service-registry.const.ts +21 -0
  51. package/src/_collections/client-safe-error.util.spec.ts +95 -0
  52. package/src/_collections/client-safe-error.util.ts +72 -0
  53. package/src/_collections/global-settings.const.ts +9 -0
  54. package/src/_collections/load-shed.util.spec.ts +62 -0
  55. package/src/_collections/load-shed.util.ts +51 -0
  56. package/src/_models/control-models/endpoint-params.control-model.ts +6 -14
  57. package/src/_models/interfaces/global-settings.interface.ts +27 -0
  58. package/src/_models/interfaces/static-client-settings.interface.ts +8 -0
  59. package/src/_modules/server/server-status/server-status.controller.ts +20 -0
  60. package/src/_services/base/archive-data.service.ts +4 -0
  61. package/src/_services/base/data.service.ts +32 -17
  62. package/src/_services/base/db.service.ts +52 -0
  63. package/src/_services/core/event-loop-diag.service.spec.ts +22 -0
  64. package/src/_services/core/event-loop-diag.service.ts +122 -0
  65. package/src/_services/server/app.server.ts +46 -1
  66. package/src/index.ts +3 -0
@@ -214,6 +214,19 @@ export interface DyNTS_Global_Settings {
214
214
  sustainedCriticalPolls?: number;
215
215
  /** A graceful-exit kódja (a wrapper ezt látja restart-jelnek). Default: 137. */
216
216
  exitCode?: number;
217
+ /**
218
+ * **Auto-load-shed middleware** (BFR-OVERSEER-009 maradék): ha true, a base App egy korai
219
+ * middleware-t mountol, ami CRITICAL memória-nyomásnál (`shouldShedLoad()`) az új, nem-esszenciális
220
+ * kéréseket gyors **503**-mal ejti — a GC levegőhöz jut, a death-spiral megelőzhető. Default: false
221
+ * (opt-in — a 503-viselkedés ops-döntés; a `shouldShedLoad()` primitív enélkül is hívható kézzel).
222
+ */
223
+ loadShedEnabled?: boolean;
224
+ /**
225
+ * Path-részek, amikre a load-shed SOSEM vonatkozik (`req.path.includes(part)` match) — az
226
+ * esszenciális útvonalak (healthcheck/readiness/diag) 503-a ÖNGÓL lenne (a restart-policy
227
+ * health-probe-ját ölné meg). Default: `['/health', '/readiness', '/status', '/diag']`.
228
+ */
229
+ loadShedExemptPathParts?: string[];
217
230
  /**
218
231
  * **Boot-grace ms** az `exitOnSustainedCritical`-hez: a guard-install UTÁNI első ennyi ms-ben a sustained-critical
219
232
  * NEM lép ki (csak logol + load-shed). A nehéz-boot appok (nagy in-memory pool-hidratálás) ELEVE GC-thrash-elnek a
@@ -223,6 +236,20 @@ export interface DyNTS_Global_Settings {
223
236
  bootGraceMs?: number;
224
237
  };
225
238
 
239
+ /**
240
+ * Event-loop-lag + connection diagnosztika (BFR-OVERSEER-007) — a „process él, HTTP néma" wedge
241
+ * belső mérése (`perf_hooks.monitorEventLoopDelay` natív histogram + aktív-handle count, ring-buffer).
242
+ * A `GET /server/diag` endpoint innen olvas. A MemoryGuard MELLÉ illeszkedik (memória ↔ loop-lag).
243
+ */
244
+ eventLoopDiag?: {
245
+ /** Bekapcsolja a diag-mérést (a base App startup telepíti). Default: true (a mérés olcsó). */
246
+ enabled: boolean;
247
+ /** Mintavételi intervallum ms-ben. Default: 5000. */
248
+ pollIntervalMs?: number;
249
+ /** Megőrzött minták max száma (ring-buffer; 120 × 5s = 10 perc). Default: 120. */
250
+ maxHistoryCount?: number;
251
+ };
252
+
226
253
  /**
227
254
  * FR-258 / SR-4 — proactive collection-growth monitor (companion to {@link memoryGuard}). Samples
228
255
  * each registered collection's document count (scan-free `estimatedDocumentCount`) on a slow
@@ -25,4 +25,12 @@ export interface DyNTS_StaticClient_Settings {
25
25
  * SPA esetén ajánlott 0, hogy a böngésző ne cache-elje az index.html-t (deploy után friss).
26
26
  */
27
27
  fallbackCacheMaxAge?: number;
28
+ /**
29
+ * API-namespace prefixek, amikre a SPA-fallback NEM vonatkozik (BFR-DYNAMOBUILDER-001):
30
+ * egy ismeretlen `GET /api/...` így 404 JSON-t kap a megtévesztő `200 + index.html` helyett
31
+ * (a SPA-fallback catch-all eddig az API-namespace-t is fedte). Default:
32
+ * `['/api', '/auth-api']` — felülírható/üríthető, ha az app más namespace-t használ,
33
+ * vagy szándékosan SPA-route-ol API-szerű path alatt.
34
+ */
35
+ fallbackExcludePrefixes?: string[];
28
36
  }
@@ -8,6 +8,8 @@ import { DyNTS_Endpoint_Params } from '../../../_models/control-models/endpoint-
8
8
  import { DyNTS_global_settings } from '../../../_collections/global-settings.const';
9
9
  import { DyNTS_DbReadiness, DyNTS_ServerStatus_ControlService } from './server-status.control-service';
10
10
  import { DyNTS_ServerStatusSnapshot_ControlService } from './server-status-snapshot.control-service';
11
+ import { DyNTS_EventLoopDiag } from '../../../_services/core/event-loop-diag.service';
12
+ import { DyNTS_MemoryGuard } from '../../../_services/core/memory-guard.service';
11
13
 
12
14
 
13
15
  /**
@@ -216,6 +218,24 @@ export abstract class DyNTS_ServerStatus_Controller<
216
218
  ],
217
219
  }),
218
220
 
221
+ // Diag-endpoint (BFR-OVERSEER-007): event-loop-lag + aktív-handle + memória-nyomás EGY helyen —
222
+ // a „process él, HTTP néma" wedge triage-ához (belső jel, amit a kívülről-néző health nem lát).
223
+ new DyNTS_Endpoint_Params({
224
+ name: 'getServerDiag',
225
+ type: DyFM_HttpCallType.get,
226
+ endpoint: '/diag',
227
+ preProcesses: this.lazyPreProcessesFor('getServerDiag'),
228
+ tasks: [
229
+ async (req: Request, res: Response): Promise<void> => {
230
+ res.send({
231
+ eventLoop: DyNTS_EventLoopDiag.getInstance().getSnapshot(),
232
+ memory: DyNTS_MemoryGuard.getInstance().pressure(),
233
+ uptimeSec: Math.round(process.uptime()),
234
+ });
235
+ },
236
+ ],
237
+ }),
238
+
219
239
  // Readiness-probe: a /health-tel (liveness) szemben a DB-elérhetőséget is ellenőrzi.
220
240
  // Public (mint a /health) — orchestrátor/LB hívja; 503 ha a DB nem ready → no traffic.
221
241
  new DyNTS_Endpoint_Params({
@@ -1,4 +1,5 @@
1
1
 
2
+ import { DyNTS_archiveServiceRegistry } from '../../_collections/archive-service-registry.const';
2
3
  import {
3
4
  DyFM_DataModel_Params,
4
5
  DyFM_DBFilterSimple,
@@ -222,3 +223,6 @@ export class DyNTS_ArchiveDataService<T extends DyFM_Metadata> extends DyNTS_Dat
222
223
  }
223
224
  }
224
225
  }
226
+
227
+ // BFR-AM-012: onregisztracio a base data-service default getArchiveDataService()-ehez (ciklus-mentes ut).
228
+ DyNTS_archiveServiceRegistry.ArchiveDataService = DyNTS_ArchiveDataService;
@@ -35,6 +35,7 @@ import { DyNTS_DBQueryOptions } from '../../_models/interfaces/db-query-options.
35
35
  import { DyNTS_DBUpdate } from '../../_models/types/db-update.type';
36
36
  import { DyNTS_GlobalService } from '../core/global.service';
37
37
  import { DyNTS_ArchiveDataService } from './archive-data.service';
38
+ import { DyNTS_archiveServiceRegistry } from '../../_collections/archive-service-registry.const';
38
39
  import { DyNTS_DBService } from './db.service';
39
40
 
40
41
 
@@ -171,12 +172,23 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
171
172
  }
172
173
 
173
174
  getArchiveDataService(): DyNTS_ArchiveDataService<T> {
175
+ // Non-throwing default (BFR-AM-012): ha a dataParams `addArchive`-ot kér, a base ELŐÁLLÍTJA a
176
+ // default archive-service-t — az `addArchive: true` így out-of-the-box működik (soft-delete →
177
+ // archív-collection), per-service boilerplate-override nélkül. A korábbi feltétlen throw a
178
+ // loadData/delete archive-útjain futásidőben crashelt (és async-kontextusban néma
179
+ // `.split`-elfedésű rejected-promise-ként jelent meg). Override továbbra is felülírhatja.
180
+ // a registry-n át példányosítunk (körkörös-import feloldás — l. archive-service-registry.const)
181
+ if (this.dataParams?.addArchive && DyNTS_archiveServiceRegistry.ArchiveDataService) {
182
+ return new DyNTS_archiveServiceRegistry.ArchiveDataService<T>(this.data, this.dataParams, this.issuer);
183
+ }
184
+
185
+ // addArchive nélkül az archive-service-hívás valódi programhiba → marad az explicit throw
174
186
  throw new DyFM_Error({
175
187
  ...this._getDefaultErrorSettings(
176
188
  'getArchiveDataService',
177
- new Error('getArchiveDataService is not implemented!')
189
+ new Error('getArchiveDataService is not implemented! (dataParams.addArchive is not set)')
178
190
  ),
179
-
191
+
180
192
  errorCode: `${this.ecBase}DyNTS-DS-GAD0`,
181
193
  });
182
194
  }
@@ -1292,13 +1304,15 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1292
1304
 
1293
1305
  await this.validateDataByPropertyParams(data, this.dataParams.properties);
1294
1306
  } catch (error) {
1295
- if (error?.errorCode?.includes?.('DyNTS-DS0-VBP') || error?.errorCode?.includes?.('DyNTS-DS0-VP')) {
1307
+ // a TELJES validacios csalad (VP/VDP/VBP/VD) valtozatlanul atmegy — a korabbi szukebb szuro a VDP*-t
1308
+ // ujra-becsomagolta (double-wrap: a 400 elveszett; BFR-FUTDEVPRO-001)
1309
+ if (error?.errorCode?.includes?.('DyNTS-DS0-V')) {
1296
1310
  throw error;
1297
1311
  } else {
1298
1312
  throw new DyFM_Error({
1299
1313
  ...this._getDefaultErrorSettings('validateForSave', error),
1300
1314
 
1301
- status: 522,
1315
+ status: 400,
1302
1316
  errorCode: `${this.ecBase}DyNTS-DS0-VD0`,
1303
1317
  });
1304
1318
  }
@@ -1363,7 +1377,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1363
1377
  const validationErrorsSet: DyFM_ValidationErrors = {};
1364
1378
  validationErrors.forEach((error: DyFM_Error) => {
1365
1379
  Object.keys(error.validationErrors).forEach((key: string) => {
1366
- validationErrors[key] = error.validationErrors[key];
1380
+ validationErrorsSet[key] = error.validationErrors[key];
1367
1381
  });
1368
1382
  });
1369
1383
 
@@ -1382,13 +1396,14 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1382
1396
  }
1383
1397
  }
1384
1398
  } catch (error) {
1385
- if (error?.errorCode?.includes?.('DyNTS-DS0-VDP')) {
1399
+ // a teljes validacios csalad atengedese (a leaf VP-hibak eddig VDP0-ba csomagolodtak ujra)
1400
+ if (error?.errorCode?.includes?.('DyNTS-DS0-V')) {
1386
1401
  throw error;
1387
1402
  } else {
1388
1403
  throw new DyFM_Error({
1389
1404
  ...this._getDefaultErrorSettings('validateByPropertyParams', error),
1390
1405
 
1391
- status: 522,
1406
+ status: 400,
1392
1407
  errorCode: `${this.ecBase}DyNTS-DS0-VDP0`,
1393
1408
  });
1394
1409
  }
@@ -1426,7 +1441,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1426
1441
  )
1427
1442
  ),
1428
1443
 
1429
- status: 522,
1444
+ status: 400,
1430
1445
  errorCode: `${this.ecBase}DyNTS-DS0-VP1`,
1431
1446
  userMessage: this.defaultValidationErrorUserMsg,
1432
1447
  validationErrors: {
@@ -1457,7 +1472,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1457
1472
  )
1458
1473
  ),
1459
1474
 
1460
- status: 522,
1475
+ status: 400,
1461
1476
  errorCode: `${this.ecBase}DyNTS-DS0-VP2`,
1462
1477
  userMessage: this.defaultValidationErrorUserMsg,
1463
1478
  validationErrors: {
@@ -1498,7 +1513,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1498
1513
  )
1499
1514
  ),
1500
1515
 
1501
- status: 522,
1516
+ status: 400,
1502
1517
  errorCode: `${this.ecBase}DyNTS-DS0-VP3`,
1503
1518
  userMessage: this.defaultValidationErrorUserMsg,
1504
1519
  validationErrors: {
@@ -1530,7 +1545,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1530
1545
  )
1531
1546
  ),
1532
1547
 
1533
- status: 522,
1548
+ status: 400,
1534
1549
  errorCode: `${this.ecBase}DyNTS-DS0-VP4`,
1535
1550
  userMessage: this.defaultValidationErrorUserMsg,
1536
1551
  validationErrors: {
@@ -1563,7 +1578,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1563
1578
  )
1564
1579
  ),
1565
1580
 
1566
- status: 522,
1581
+ status: 400,
1567
1582
  errorCode: `${this.ecBase}DyNTS-DS0-VP5`,
1568
1583
  userMessage: this.defaultValidationErrorUserMsg,
1569
1584
  validationErrors: {
@@ -1597,7 +1612,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1597
1612
  )
1598
1613
  ),
1599
1614
 
1600
- status: 522,
1615
+ status: 400,
1601
1616
  errorCode: `${this.ecBase}DyNTS-DS0-VP6`,
1602
1617
  userMessage: this.defaultValidationErrorUserMsg,
1603
1618
  validationErrors: {
@@ -1628,7 +1643,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1628
1643
  )
1629
1644
  ),
1630
1645
 
1631
- status: 522,
1646
+ status: 400,
1632
1647
  errorCode: `${this.ecBase}DyNTS-DS0-VP7`,
1633
1648
  userMessage: this.defaultValidationErrorUserMsg,
1634
1649
  validationErrors: {
@@ -1663,7 +1678,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1663
1678
  )
1664
1679
  ),
1665
1680
 
1666
- status: 522,
1681
+ status: 400,
1667
1682
  errorCode: `${this.ecBase}DyNTS-DS0-VP8`,
1668
1683
  userMessage: this.defaultValidationErrorUserMsg,
1669
1684
  validationErrors: {
@@ -1703,7 +1718,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1703
1718
  )
1704
1719
  ),
1705
1720
 
1706
- status: 522,
1721
+ status: 400,
1707
1722
  errorCode: `${this.ecBase}VP9`,
1708
1723
  userMessage: this.defaultValidationErrorUserMsg,
1709
1724
  validationErrors: {
@@ -1733,7 +1748,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1733
1748
  )
1734
1749
  ),
1735
1750
 
1736
- status: 522,
1751
+ status: 400,
1737
1752
  errorCode: `${this.ecBase}DyNTS-DS0-VP10`,
1738
1753
  userMessage: this.defaultValidationErrorUserMsg,
1739
1754
  validationErrors: {
@@ -1011,6 +1011,58 @@ export class DyNTS_DBService<T extends DyFM_Metadata> {
1011
1011
  return this.stringifyDataId(data, 'findByIdAndUpdate');
1012
1012
  }
1013
1013
 
1014
+ /**
1015
+ * #MONGOOSE FUNCTION
1016
+ * Atomi compare-and-set: filter-alapú find + update EGY műveletben, a MÓDOSÍTOTT dokumentum
1017
+ * visszaadásával (BFR-FDPTOKENSERVICE-006).
1018
+ *
1019
+ * A `findByIdAndUpdate` (id-only, nem filterezhető) és az `updateOne` (filterezhető, de void —
1020
+ * nem különíthető el a winner/loser konkurens state-átmenetnél) egyike sem elég CAS-hoz: egy
1021
+ * state-machine `transitionStatus`-jellegű átmenetnél a filter a FELTÉTEL (pl. `{_id, status:
1022
+ * 'locked'}`), és a visszatérő érték dönti el, hogy EZ a hívás nyerte-e az átmenetet.
1023
+ * `null` = nincs match (a feltétel már nem áll — a versenytárs nyert / state-collision).
1024
+ *
1025
+ * A `__lastModified`/`forbidModify` szemantika a `findByIdAndUpdate`-tel EGYEZŐ (egységes
1026
+ * update-viselkedés a base-ben); a `{ new: true }` a MÓDOSÍTOTT dokumentumot adja vissza.
1027
+ */
1028
+ async findOneAndUpdate(
1029
+ filterBy: DyFM_DBFilter<T>,
1030
+ update: DyNTS_DBUpdate<T>,
1031
+ issuer: string,
1032
+ dontUpdateModified?: boolean
1033
+ ): Promise<T | null> {
1034
+ if (this.dataParams.forbidModify) {
1035
+ await this.modifyThrowMethod(update, issuer);
1036
+ }
1037
+
1038
+ if (!dontUpdateModified) {
1039
+ update.__lastModified = new Date();
1040
+ update.__lastModifiedBy = issuer;
1041
+ }
1042
+
1043
+ const data: T | null = await this.dataModel
1044
+ .findOneAndUpdate(filterBy, update, { new: true })
1045
+ .then((res): T | null => res?.toObject() as T ?? null)
1046
+ .catch((error): never => {
1047
+ throw new DyFM_Error({
1048
+ ...this._getDefaultErrorSettings('findOneAndUpdate', error, issuer),
1049
+ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-FOU0`,
1050
+ additionalContent: { filterBy, update },
1051
+ message:
1052
+ `findOneAndUpdate "${this.dataParams.dataName}" was unsuccessful (NTS DB)` +
1053
+ `\n error: ${DyFM_Error.getErrorMessage(error)}`,
1054
+ issuer,
1055
+ });
1056
+ });
1057
+
1058
+ // no-match (CAS lost / feltétel nem áll): a null ÉRVÉNYES eredmény, nem hiba
1059
+ if (!data) {
1060
+ return null;
1061
+ }
1062
+
1063
+ return this.stringifyDataId(data, 'findOneAndUpdate');
1064
+ }
1065
+
1014
1066
  /**
1015
1067
  * #MONGOOSE FUNCTION
1016
1068
  * Find the data first by any of its parameters
@@ -0,0 +1,22 @@
1
+ import { DyNTS_EventLoopDiag, DyNTS_EventLoopSample } from './event-loop-diag.service';
2
+
3
+ // Az event-loop-diag (BFR-OVERSEER-007) alap-tesztjei: snapshot-shape + install-idempotencia.
4
+ // A tényleges lag-értékeket nem asszertáljuk (env-függő) — a mérő-plumbing működését igen.
5
+ describe('| DyNTS_EventLoopDiag', (): void => {
6
+
7
+ it('| getSnapshot install előtt is biztonságos (üres kép)', (): void => {
8
+ const snapshot: { installed: boolean; current: DyNTS_EventLoopSample | null; history: DyNTS_EventLoopSample[] } =
9
+ DyNTS_EventLoopDiag.getInstance().getSnapshot();
10
+
11
+ expect(Array.isArray(snapshot.history)).toBeTrue();
12
+ });
13
+
14
+ it('| install idempotens és a snapshot installed-et jelez', (): void => {
15
+ const diag: DyNTS_EventLoopDiag = DyNTS_EventLoopDiag.getInstance();
16
+
17
+ diag.install();
18
+ diag.install();
19
+
20
+ expect(diag.getSnapshot().installed).toBeTrue();
21
+ });
22
+ });
@@ -0,0 +1,122 @@
1
+ import { monitorEventLoopDelay, IntervalHistogram } from 'perf_hooks';
2
+
3
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
4
+
5
+ import { DyNTS_global_settings } from '../../_collections/global-settings.const';
6
+
7
+ /** Egy event-loop-lag mintavétel (a gördülő history eleme). */
8
+ export interface DyNTS_EventLoopSample {
9
+ at: number;
10
+ /** p50 lag ms-ben (a poll-ablakban). */
11
+ p50Ms: number;
12
+ /** p95 lag ms-ben. */
13
+ p95Ms: number;
14
+ /** max lag ms-ben — a „process él, HTTP néma" wedge fő jele. */
15
+ maxMs: number;
16
+ /** Aktív handle-k száma (socket/timer/fd) — connection-leak jel. */
17
+ activeHandles: number;
18
+ }
19
+
20
+ /**
21
+ * Event-loop-lag + connection diagnosztika (BFR-OVERSEER-007) — a „process él, de a HTTP néma"
22
+ * jelenség (event-loop blokk / wedge) eddig LÁTHATATLAN volt: a MemoryGuard a heap-et figyeli,
23
+ * a healthcheck kívülről néz — a loop-lag magát a tünetet méri, belülről.
24
+ *
25
+ * A `perf_hooks.monitorEventLoopDelay` NATÍV histogramjára épül (elhanyagolható overhead),
26
+ * poll-onként mintát tesz egy ring-bufferbe; a `getSnapshot()` a `/server/diag` endpoint (és
27
+ * bármely fogyasztó) számára adja vissza az aktuális + közelmúlt-képet. A MemoryGuard MELLÉ
28
+ * illeszkedik (nem duplikálja): memória-nyomás = MemoryGuard.pressure(); loop-lag = ez.
29
+ *
30
+ * Install: a base App startup (a MemoryGuard-install mintájára), `eventLoopDiag.enabled` gate-tel
31
+ * (default: true — a mérés olcsó; opt-out lehetséges).
32
+ */
33
+ export class DyNTS_EventLoopDiag {
34
+
35
+ private static instance: DyNTS_EventLoopDiag;
36
+
37
+ static getInstance(): DyNTS_EventLoopDiag {
38
+ if (!DyNTS_EventLoopDiag.instance) {
39
+ DyNTS_EventLoopDiag.instance = new DyNTS_EventLoopDiag();
40
+ }
41
+
42
+ return DyNTS_EventLoopDiag.instance;
43
+ }
44
+
45
+ private monitor: IntervalHistogram | null = null;
46
+ private pollTimer: NodeJS.Timeout | null = null;
47
+ private history: DyNTS_EventLoopSample[] = [];
48
+ private installed: boolean = false;
49
+
50
+ /** A diag-mérés telepítése (idempotens; a settings-gate itt érvényesül). */
51
+ install(): void {
52
+ if (this.installed) {
53
+ return;
54
+ }
55
+
56
+ const settings: { enabled?: boolean; pollIntervalMs?: number; maxHistoryCount?: number } =
57
+ DyNTS_global_settings.eventLoopDiag ?? {};
58
+
59
+ if (settings.enabled === false) {
60
+ return;
61
+ }
62
+
63
+ const pollIntervalMs: number = settings.pollIntervalMs ?? 5000;
64
+ const maxHistoryCount: number = settings.maxHistoryCount ?? 120;
65
+
66
+ // natív histogram — 10ms felbontás bőven elég a wedge-detektáláshoz (a jel: 100ms+ lag)
67
+ this.monitor = monitorEventLoopDelay({ resolution: 10 });
68
+ this.monitor.enable();
69
+
70
+ this.pollTimer = setInterval((): void => {
71
+ if (!this.monitor) {
72
+ return;
73
+ }
74
+
75
+ // a histogram ns-ben mér → ms-re váltunk; poll után reset (ablakonkénti kép)
76
+ this.history.push({
77
+ at: Date.now(),
78
+ p50Ms: Math.round(this.monitor.percentile(50) / 1e6),
79
+ p95Ms: Math.round(this.monitor.percentile(95) / 1e6),
80
+ maxMs: Math.round(this.monitor.max / 1e6),
81
+ activeHandles: this.getActiveHandleCount(),
82
+ });
83
+ this.monitor.reset();
84
+
85
+ if (this.history.length > maxHistoryCount) {
86
+ this.history = this.history.slice(-maxHistoryCount);
87
+ }
88
+ }, pollIntervalMs);
89
+
90
+ // a poll-timer NE tartsa életben a processzt
91
+ this.pollTimer.unref?.();
92
+
93
+ this.installed = true;
94
+ DyFM_Log.log(`Event-loop diag installed (poll: ${pollIntervalMs}ms, history: ${maxHistoryCount})`);
95
+ }
96
+
97
+ /** A diag-pillanatkép — a `/server/diag` endpoint (és bármely fogyasztó) forrása. */
98
+ getSnapshot(): {
99
+ installed: boolean;
100
+ current: DyNTS_EventLoopSample | null;
101
+ history: DyNTS_EventLoopSample[];
102
+ } {
103
+ return {
104
+ installed: this.installed,
105
+ current: this.history.length ? this.history[this.history.length - 1] : null,
106
+ history: this.history,
107
+ };
108
+ }
109
+
110
+ /** Aktív handle-k száma — best-effort (a nem-publikus API hiányát 0-val toleráljuk). */
111
+ private getActiveHandleCount(): number {
112
+ // a _getActiveHandles nem publikus API — best-effort diag-jel (connection/timer-leak trend)
113
+ const getHandles: (() => unknown[]) | undefined =
114
+ (process as unknown as { _getActiveHandles?: () => unknown[] })._getActiveHandles;
115
+
116
+ try {
117
+ return getHandles ? getHandles().length : 0;
118
+ } catch {
119
+ return 0;
120
+ }
121
+ }
122
+ }
@@ -30,8 +30,10 @@ import {
30
30
  import { DyNTS_defaultFallbackCacheMaxAge } from '../../_collections/default-fallback-cache-max-age.const';
31
31
  import { DyNTS_defaultNotFoundPageHtml } from '../../_collections/default-not-found-page.const';
32
32
  import { DyNTS_global_settings } from '../../_collections/global-settings.const';
33
+ import { DyNTS_ClientSafeError_Util } from '../../_collections/client-safe-error.util';
33
34
  import { DyNTS_getEnvironment_settings } from '../../_collections/get-environment-settings.util';
34
35
  import { DyNTS_createSecurityHeadersMiddleware } from '../../_collections/security-headers.util';
36
+ import { DyNTS_createLoadShedMiddleware } from '../../_collections/load-shed.util';
35
37
  import { DyNTS_SecurityHeaders_Settings } from '../../_models/interfaces/security-headers-settings.interface';
36
38
  import { startMongoReconnectGuard } from '../../_collections/mongo-reconnect-guard.util';
37
39
  import { DyNTS_resolveStaticCacheControlHeader } from '../../_collections/static-cache-control.util';
@@ -61,6 +63,7 @@ import { DyNTS_SingletonService } from '../base/singleton.service';
61
63
  import { DyNTS_GlobalService } from '../core/global.service';
62
64
  import { DyNTS_CollectionGrowthMonitor } from '../core/collection-growth-monitor.service';
63
65
  import { DyNTS_MemoryGuard } from '../core/memory-guard.service';
66
+ import { DyNTS_EventLoopDiag } from '../core/event-loop-diag.service';
64
67
  import { DyNTS_RoutingModule } from '../route/routing-module.service';
65
68
  import { DyNTS_getStarRoute } from '../../_collections/star.controller';
66
69
 
@@ -593,6 +596,14 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
593
596
  DyFM_Log.warn('[DyNTS_MemoryGuard] auto-install skipped (non-fatal):', memoryGuardError);
594
597
  }
595
598
 
599
+ // BFR-OVERSEER-007 — event-loop-lag diag a MemoryGuard MELLÉ: a „process él, HTTP néma" wedge
600
+ // belső mérése (natív histogram, olcsó). Observation-only, sosem dob; a /server/diag olvas belőle.
601
+ try {
602
+ DyNTS_EventLoopDiag.getInstance().install();
603
+ } catch (eventLoopDiagError: unknown) {
604
+ DyFM_Log.warn('[DyNTS_EventLoopDiag] auto-install skipped (non-fatal):', eventLoopDiagError);
605
+ }
606
+
596
607
  // FR-258 / SR-4 — proactive collection-growth monitor (companion to the MemoryGuard). Warns
597
608
  // into the Errors-sink when a collection grows unbounded, BEFORE it can be loaded into heap and
598
609
  // OOM. Default-on, observation-only, never throws.
@@ -1285,6 +1296,10 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1285
1296
  this.mountSecurityHeaders(this.openExpress);
1286
1297
  // FR-041 — CORS allowlist enforcement. No-op if getCorsSettings() is not overridden.
1287
1298
  this.mountCors(this.openExpress);
1299
+ // BFR-OVERSEER-009 — opt-in auto-load-shed 503 CRITICAL memória-nyomásnál (esszenciális utak kivételével).
1300
+ if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) {
1301
+ this.openExpress.use(DyNTS_createLoadShedMiddleware());
1302
+ }
1288
1303
  }
1289
1304
 
1290
1305
  /**
@@ -1299,6 +1314,10 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1299
1314
  this.mountSecurityHeaders(this.secureExpress);
1300
1315
  // FR-041 — CORS allowlist enforcement (same as open express).
1301
1316
  this.mountCors(this.secureExpress);
1317
+ // BFR-OVERSEER-009 — opt-in auto-load-shed 503 (mint az open expressen).
1318
+ if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) {
1319
+ this.secureExpress.use(DyNTS_createLoadShedMiddleware());
1320
+ }
1302
1321
 
1303
1322
  const options = {
1304
1323
  key: FileSystem.readFileSync(this._cert.keyPath),
@@ -1482,7 +1501,10 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1482
1501
 
1483
1502
  await DyNTS_GlobalService.globalErrorHandler?.(d_error, req, res);
1484
1503
 
1485
- res.send(error)
1504
+ // Kliens-safe strip (BFR-FDPAUTHSERVICE-002): az express-error út eddig NYERS error-t küldött
1505
+ // (0 strip) — DyFM-nél belső stack/verzió/issuer, axios-jellegű error-nál akár auth-header is
1506
+ // kimehetett. A strip-szabályok SSOT-ja a DyNTS_ClientSafeError_Util.
1507
+ res.send(DyNTS_ClientSafeError_Util.toClientSafe(error))
1486
1508
  } else {
1487
1509
  DyFM_Log.H_error(
1488
1510
  'WTF??? express error; without error?...' +
@@ -1777,15 +1799,30 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1777
1799
  const fallbackMaxAge: number =
1778
1800
  settings.fallbackCacheMaxAge ?? DyNTS_defaultFallbackCacheMaxAge;
1779
1801
 
1802
+ // API-namespace guard (BFR-DYNAMOBUILDER-001): a catch-all fallback NE fedje az API-prefixeket —
1803
+ // egy ismeretlen `GET /api/...` 404 JSON-t kapjon, ne megtévesztő `200 + index.html`-t.
1804
+ // Konfigurálható (fallbackExcludePrefixes); default a bevett FDP namespace-pár.
1805
+ const excludePrefixes: string[] = settings.fallbackExcludePrefixes ?? ['/api', '/auth-api'];
1806
+ const isApiNamespacePath = (path: string): boolean =>
1807
+ excludePrefixes.some((prefix: string): boolean => path === prefix || path.startsWith(`${prefix}/`));
1808
+
1780
1809
  if (this.openExpress) {
1781
1810
  this.openExpress.use('/', staticMiddleware);
1782
1811
  if (settings.fallbackPath) {
1783
1812
  this.openExpress.get('*', (req: Express.Request, res: Express.Response): void => {
1813
+ if (isApiNamespacePath(req.path)) {
1814
+ res.status(404).send({ message: `Unknown API endpoint: ${req.path}` });
1815
+ return;
1816
+ }
1784
1817
  res.setHeader('Cache-Control', `max-age=${fallbackMaxAge}`);
1785
1818
  res.sendFile(settings.fallbackPath!, { root: absoluteRoot });
1786
1819
  });
1787
1820
  } else {
1788
1821
  this.openExpress.get('*', (req: Express.Request, res: Express.Response): void => {
1822
+ if (isApiNamespacePath(req.path)) {
1823
+ res.status(404).send({ message: `Unknown API endpoint: ${req.path}` });
1824
+ return;
1825
+ }
1789
1826
  res.status(404).type('html').send(DyNTS_defaultNotFoundPageHtml);
1790
1827
  });
1791
1828
  }
@@ -1795,11 +1832,19 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1795
1832
  this.secureExpress.use('/', staticMiddleware);
1796
1833
  if (settings.fallbackPath) {
1797
1834
  this.secureExpress.get('*', (req: Express.Request, res: Express.Response): void => {
1835
+ if (isApiNamespacePath(req.path)) {
1836
+ res.status(404).send({ message: `Unknown API endpoint: ${req.path}` });
1837
+ return;
1838
+ }
1798
1839
  res.setHeader('Cache-Control', `max-age=${fallbackMaxAge}`);
1799
1840
  res.sendFile(settings.fallbackPath!, { root: absoluteRoot });
1800
1841
  });
1801
1842
  } else {
1802
1843
  this.secureExpress.get('*', (req: Express.Request, res: Express.Response): void => {
1844
+ if (isApiNamespacePath(req.path)) {
1845
+ res.status(404).send({ message: `Unknown API endpoint: ${req.path}` });
1846
+ return;
1847
+ }
1803
1848
  res.status(404).type('html').send(DyNTS_defaultNotFoundPageHtml);
1804
1849
  });
1805
1850
  }
package/src/index.ts CHANGED
@@ -54,6 +54,9 @@ export * from './_models/interfaces/cors-settings.interface';
54
54
  export * from './_models/interfaces/security-headers-settings.interface';
55
55
  export * from './_models/interfaces/db-query-options.interface';
56
56
  export * from './_collections/security-headers.util';
57
+ export * from './_collections/client-safe-error.util';
58
+ export * from './_collections/load-shed.util';
59
+ export * from './_services/core/event-loop-diag.service';
57
60
 
58
61
  // models/CONTROL MODELS
59
62
  export * from './_models/control-models/api-call-params.control-model';