@futdevpro/nts-dynamo 1.15.100 → 1.15.102

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 (60) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1517 -1537
  2. package/.dynamo/logs/cicd-pipeline/status.json +27 -27
  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/global-settings.const.d.ts.map +1 -1
  8. package/build/_collections/global-settings.const.js +9 -0
  9. package/build/_collections/global-settings.const.js.map +1 -1
  10. package/build/_collections/load-shed.util.d.ts +16 -0
  11. package/build/_collections/load-shed.util.d.ts.map +1 -0
  12. package/build/_collections/load-shed.util.js +43 -0
  13. package/build/_collections/load-shed.util.js.map +1 -0
  14. package/build/_models/interfaces/global-settings.interface.d.ts +26 -0
  15. package/build/_models/interfaces/global-settings.interface.d.ts.map +1 -1
  16. package/build/_models/interfaces/static-client-settings.interface.d.ts +8 -0
  17. package/build/_models/interfaces/static-client-settings.interface.d.ts.map +1 -1
  18. package/build/_modules/server/server-status/server-status.controller.d.ts.map +1 -1
  19. package/build/_modules/server/server-status/server-status.controller.js +19 -0
  20. package/build/_modules/server/server-status/server-status.controller.js.map +1 -1
  21. package/build/_modules/socket/app-extended.server.d.ts.map +1 -1
  22. package/build/_modules/socket/app-extended.server.js +14 -4
  23. package/build/_modules/socket/app-extended.server.js.map +1 -1
  24. package/build/_services/base/archive-data.service.d.ts.map +1 -1
  25. package/build/_services/base/archive-data.service.js +3 -0
  26. package/build/_services/base/archive-data.service.js.map +1 -1
  27. package/build/_services/base/data.service.d.ts.map +1 -1
  28. package/build/_services/base/data.service.js +30 -16
  29. package/build/_services/base/data.service.js.map +1 -1
  30. package/build/_services/base/db.service.d.ts +15 -0
  31. package/build/_services/base/db.service.d.ts.map +1 -1
  32. package/build/_services/base/db.service.js +41 -0
  33. package/build/_services/base/db.service.js.map +1 -1
  34. package/build/_services/core/event-loop-diag.service.d.ts +44 -0
  35. package/build/_services/core/event-loop-diag.service.d.ts.map +1 -0
  36. package/build/_services/core/event-loop-diag.service.js +89 -0
  37. package/build/_services/core/event-loop-diag.service.js.map +1 -0
  38. package/build/_services/server/app.server.d.ts.map +1 -1
  39. package/build/_services/server/app.server.js +39 -0
  40. package/build/_services/server/app.server.js.map +1 -1
  41. package/build/index.d.ts +2 -0
  42. package/build/index.d.ts.map +1 -1
  43. package/build/index.js +2 -0
  44. package/build/index.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/_collections/archive-service-registry.const.ts +21 -0
  47. package/src/_collections/global-settings.const.ts +9 -0
  48. package/src/_collections/load-shed.util.spec.ts +62 -0
  49. package/src/_collections/load-shed.util.ts +51 -0
  50. package/src/_models/interfaces/global-settings.interface.ts +27 -0
  51. package/src/_models/interfaces/static-client-settings.interface.ts +8 -0
  52. package/src/_modules/server/server-status/server-status.controller.ts +20 -0
  53. package/src/_modules/socket/app-extended.server.ts +16 -4
  54. package/src/_services/base/archive-data.service.ts +4 -0
  55. package/src/_services/base/data.service.ts +32 -17
  56. package/src/_services/base/db.service.ts +52 -0
  57. package/src/_services/core/event-loop-diag.service.spec.ts +22 -0
  58. package/src/_services/core/event-loop-diag.service.ts +122 -0
  59. package/src/_services/server/app.server.ts +41 -0
  60. package/src/index.ts +2 -0
@@ -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
+ }
@@ -33,6 +33,7 @@ import { DyNTS_global_settings } from '../../_collections/global-settings.const'
33
33
  import { DyNTS_ClientSafeError_Util } from '../../_collections/client-safe-error.util';
34
34
  import { DyNTS_getEnvironment_settings } from '../../_collections/get-environment-settings.util';
35
35
  import { DyNTS_createSecurityHeadersMiddleware } from '../../_collections/security-headers.util';
36
+ import { DyNTS_createLoadShedMiddleware } from '../../_collections/load-shed.util';
36
37
  import { DyNTS_SecurityHeaders_Settings } from '../../_models/interfaces/security-headers-settings.interface';
37
38
  import { startMongoReconnectGuard } from '../../_collections/mongo-reconnect-guard.util';
38
39
  import { DyNTS_resolveStaticCacheControlHeader } from '../../_collections/static-cache-control.util';
@@ -62,6 +63,7 @@ import { DyNTS_SingletonService } from '../base/singleton.service';
62
63
  import { DyNTS_GlobalService } from '../core/global.service';
63
64
  import { DyNTS_CollectionGrowthMonitor } from '../core/collection-growth-monitor.service';
64
65
  import { DyNTS_MemoryGuard } from '../core/memory-guard.service';
66
+ import { DyNTS_EventLoopDiag } from '../core/event-loop-diag.service';
65
67
  import { DyNTS_RoutingModule } from '../route/routing-module.service';
66
68
  import { DyNTS_getStarRoute } from '../../_collections/star.controller';
67
69
 
@@ -594,6 +596,14 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
594
596
  DyFM_Log.warn('[DyNTS_MemoryGuard] auto-install skipped (non-fatal):', memoryGuardError);
595
597
  }
596
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
+
597
607
  // FR-258 / SR-4 — proactive collection-growth monitor (companion to the MemoryGuard). Warns
598
608
  // into the Errors-sink when a collection grows unbounded, BEFORE it can be loaded into heap and
599
609
  // OOM. Default-on, observation-only, never throws.
@@ -1286,6 +1296,10 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1286
1296
  this.mountSecurityHeaders(this.openExpress);
1287
1297
  // FR-041 — CORS allowlist enforcement. No-op if getCorsSettings() is not overridden.
1288
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
+ }
1289
1303
  }
1290
1304
 
1291
1305
  /**
@@ -1300,6 +1314,10 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1300
1314
  this.mountSecurityHeaders(this.secureExpress);
1301
1315
  // FR-041 — CORS allowlist enforcement (same as open express).
1302
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
+ }
1303
1321
 
1304
1322
  const options = {
1305
1323
  key: FileSystem.readFileSync(this._cert.keyPath),
@@ -1781,15 +1799,30 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1781
1799
  const fallbackMaxAge: number =
1782
1800
  settings.fallbackCacheMaxAge ?? DyNTS_defaultFallbackCacheMaxAge;
1783
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
+
1784
1809
  if (this.openExpress) {
1785
1810
  this.openExpress.use('/', staticMiddleware);
1786
1811
  if (settings.fallbackPath) {
1787
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
+ }
1788
1817
  res.setHeader('Cache-Control', `max-age=${fallbackMaxAge}`);
1789
1818
  res.sendFile(settings.fallbackPath!, { root: absoluteRoot });
1790
1819
  });
1791
1820
  } else {
1792
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
+ }
1793
1826
  res.status(404).type('html').send(DyNTS_defaultNotFoundPageHtml);
1794
1827
  });
1795
1828
  }
@@ -1799,11 +1832,19 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1799
1832
  this.secureExpress.use('/', staticMiddleware);
1800
1833
  if (settings.fallbackPath) {
1801
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
+ }
1802
1839
  res.setHeader('Cache-Control', `max-age=${fallbackMaxAge}`);
1803
1840
  res.sendFile(settings.fallbackPath!, { root: absoluteRoot });
1804
1841
  });
1805
1842
  } else {
1806
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
+ }
1807
1848
  res.status(404).type('html').send(DyNTS_defaultNotFoundPageHtml);
1808
1849
  });
1809
1850
  }
package/src/index.ts CHANGED
@@ -55,6 +55,8 @@ 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
57
  export * from './_collections/client-safe-error.util';
58
+ export * from './_collections/load-shed.util';
59
+ export * from './_services/core/event-loop-diag.service';
58
60
 
59
61
  // models/CONTROL MODELS
60
62
  export * from './_models/control-models/api-call-params.control-model';