@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.
- package/.dynamo/logs/cicd-pipeline/output.log +1517 -1537
- package/.dynamo/logs/cicd-pipeline/status.json +27 -27
- package/build/_collections/archive-service-registry.const.d.ts +16 -0
- package/build/_collections/archive-service-registry.const.d.ts.map +1 -0
- package/build/_collections/archive-service-registry.const.js +15 -0
- package/build/_collections/archive-service-registry.const.js.map +1 -0
- package/build/_collections/global-settings.const.d.ts.map +1 -1
- package/build/_collections/global-settings.const.js +9 -0
- package/build/_collections/global-settings.const.js.map +1 -1
- package/build/_collections/load-shed.util.d.ts +16 -0
- package/build/_collections/load-shed.util.d.ts.map +1 -0
- package/build/_collections/load-shed.util.js +43 -0
- package/build/_collections/load-shed.util.js.map +1 -0
- package/build/_models/interfaces/global-settings.interface.d.ts +26 -0
- package/build/_models/interfaces/global-settings.interface.d.ts.map +1 -1
- package/build/_models/interfaces/static-client-settings.interface.d.ts +8 -0
- package/build/_models/interfaces/static-client-settings.interface.d.ts.map +1 -1
- package/build/_modules/server/server-status/server-status.controller.d.ts.map +1 -1
- package/build/_modules/server/server-status/server-status.controller.js +19 -0
- package/build/_modules/server/server-status/server-status.controller.js.map +1 -1
- package/build/_modules/socket/app-extended.server.d.ts.map +1 -1
- package/build/_modules/socket/app-extended.server.js +14 -4
- package/build/_modules/socket/app-extended.server.js.map +1 -1
- package/build/_services/base/archive-data.service.d.ts.map +1 -1
- package/build/_services/base/archive-data.service.js +3 -0
- package/build/_services/base/archive-data.service.js.map +1 -1
- package/build/_services/base/data.service.d.ts.map +1 -1
- package/build/_services/base/data.service.js +30 -16
- package/build/_services/base/data.service.js.map +1 -1
- package/build/_services/base/db.service.d.ts +15 -0
- package/build/_services/base/db.service.d.ts.map +1 -1
- package/build/_services/base/db.service.js +41 -0
- package/build/_services/base/db.service.js.map +1 -1
- package/build/_services/core/event-loop-diag.service.d.ts +44 -0
- package/build/_services/core/event-loop-diag.service.d.ts.map +1 -0
- package/build/_services/core/event-loop-diag.service.js +89 -0
- package/build/_services/core/event-loop-diag.service.js.map +1 -0
- package/build/_services/server/app.server.d.ts.map +1 -1
- package/build/_services/server/app.server.js +39 -0
- package/build/_services/server/app.server.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/package.json +3 -3
- package/src/_collections/archive-service-registry.const.ts +21 -0
- package/src/_collections/global-settings.const.ts +9 -0
- package/src/_collections/load-shed.util.spec.ts +62 -0
- package/src/_collections/load-shed.util.ts +51 -0
- package/src/_models/interfaces/global-settings.interface.ts +27 -0
- package/src/_models/interfaces/static-client-settings.interface.ts +8 -0
- package/src/_modules/server/server-status/server-status.controller.ts +20 -0
- package/src/_modules/socket/app-extended.server.ts +16 -4
- package/src/_services/base/archive-data.service.ts +4 -0
- package/src/_services/base/data.service.ts +32 -17
- package/src/_services/base/db.service.ts +52 -0
- package/src/_services/core/event-loop-diag.service.spec.ts +22 -0
- package/src/_services/core/event-loop-diag.service.ts +122 -0
- package/src/_services/server/app.server.ts +41 -0
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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';
|