@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.
- package/.dynamo/logs/cicd-pipeline/output.log +1855 -1791
- package/.dynamo/logs/cicd-pipeline/status.json +29 -29
- 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/client-safe-error.util.d.ts +33 -0
- package/build/_collections/client-safe-error.util.d.ts.map +1 -0
- package/build/_collections/client-safe-error.util.js +68 -0
- package/build/_collections/client-safe-error.util.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/control-models/endpoint-params.control-model.d.ts.map +1 -1
- package/build/_models/control-models/endpoint-params.control-model.js +6 -11
- package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
- 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/_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 +44 -1
- package/build/_services/server/app.server.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/_collections/archive-service-registry.const.ts +21 -0
- package/src/_collections/client-safe-error.util.spec.ts +95 -0
- package/src/_collections/client-safe-error.util.ts +72 -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/control-models/endpoint-params.control-model.ts +6 -14
- 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/_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 +46 -1
- 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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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';
|