@futdevpro/nts-dynamo 1.15.118 → 1.15.120

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 (91) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1845 -1703
  2. package/.dynamo/logs/cicd-pipeline/status.json +42 -36
  3. package/build/_models/control-models/endpoint-params.control-model.d.ts +5 -0
  4. package/build/_models/control-models/endpoint-params.control-model.d.ts.map +1 -1
  5. package/build/_models/control-models/endpoint-params.control-model.js +41 -11
  6. package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
  7. package/build/_models/data-models/schema-migration.data-model.d.ts +29 -0
  8. package/build/_models/data-models/schema-migration.data-model.d.ts.map +1 -0
  9. package/build/_models/data-models/schema-migration.data-model.js +47 -0
  10. package/build/_models/data-models/schema-migration.data-model.js.map +1 -0
  11. package/build/_models/interfaces/migration-entry.interface.d.ts +20 -0
  12. package/build/_models/interfaces/migration-entry.interface.d.ts.map +1 -0
  13. package/build/_models/interfaces/migration-entry.interface.js +3 -0
  14. package/build/_models/interfaces/migration-entry.interface.js.map +1 -0
  15. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts +6 -1
  16. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts.map +1 -1
  17. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js +40 -10
  18. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js.map +1 -1
  19. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts +9 -0
  20. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts.map +1 -1
  21. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js +12 -0
  22. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js.map +1 -1
  23. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts +12 -2
  24. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts.map +1 -1
  25. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js +54 -7
  26. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js.map +1 -1
  27. package/build/_modules/server/server-status/server-status.control-service.d.ts +6 -0
  28. package/build/_modules/server/server-status/server-status.control-service.d.ts.map +1 -1
  29. package/build/_modules/server/server-status/server-status.control-service.js +10 -0
  30. package/build/_modules/server/server-status/server-status.control-service.js.map +1 -1
  31. package/build/_modules/server/server-status/server-status.controller.d.ts.map +1 -1
  32. package/build/_modules/server/server-status/server-status.controller.js +26 -0
  33. package/build/_modules/server/server-status/server-status.controller.js.map +1 -1
  34. package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts +11 -1
  35. package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts.map +1 -1
  36. package/build/_modules/socket/_models/socket-server-service-params.control-model.js +12 -1
  37. package/build/_modules/socket/_models/socket-server-service-params.control-model.js.map +1 -1
  38. package/build/_modules/socket/_services/socket-server.service.d.ts +2 -0
  39. package/build/_modules/socket/_services/socket-server.service.d.ts.map +1 -1
  40. package/build/_modules/socket/_services/socket-server.service.js +2 -0
  41. package/build/_modules/socket/_services/socket-server.service.js.map +1 -1
  42. package/build/_modules/socket/app-extended.server.d.ts.map +1 -1
  43. package/build/_modules/socket/app-extended.server.js +13 -8
  44. package/build/_modules/socket/app-extended.server.js.map +1 -1
  45. package/build/_services/base/db.service.d.ts +14 -0
  46. package/build/_services/base/db.service.d.ts.map +1 -1
  47. package/build/_services/base/db.service.js +27 -0
  48. package/build/_services/base/db.service.js.map +1 -1
  49. package/build/_services/core/api.service.d.ts.map +1 -1
  50. package/build/_services/core/api.service.js +29 -9
  51. package/build/_services/core/api.service.js.map +1 -1
  52. package/build/_services/core/db-stats.service.d.ts +34 -0
  53. package/build/_services/core/db-stats.service.d.ts.map +1 -0
  54. package/build/_services/core/db-stats.service.js +65 -0
  55. package/build/_services/core/db-stats.service.js.map +1 -0
  56. package/build/_services/core/diagnostics.service.d.ts +43 -0
  57. package/build/_services/core/diagnostics.service.d.ts.map +1 -0
  58. package/build/_services/core/diagnostics.service.js +122 -0
  59. package/build/_services/core/diagnostics.service.js.map +1 -0
  60. package/build/_services/core/migration-runner.service.d.ts +40 -0
  61. package/build/_services/core/migration-runner.service.d.ts.map +1 -0
  62. package/build/_services/core/migration-runner.service.js +75 -0
  63. package/build/_services/core/migration-runner.service.js.map +1 -0
  64. package/build/index.d.ts +5 -0
  65. package/build/index.d.ts.map +1 -1
  66. package/build/index.js +6 -0
  67. package/build/index.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/_models/control-models/endpoint-params.control-model.ts +44 -16
  70. package/src/_models/data-models/schema-migration.data-model.ts +50 -0
  71. package/src/_models/interfaces/migration-entry.interface.ts +19 -0
  72. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.spec.ts +16 -0
  73. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.ts +50 -14
  74. package/src/_modules/local-vector-search/_services/lvs-vector-persist.data-service.ts +22 -0
  75. package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.spec.ts +5 -3
  76. package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.ts +62 -11
  77. package/src/_modules/server/server-status/server-status.control-service.ts +12 -0
  78. package/src/_modules/server/server-status/server-status.controller.ts +28 -0
  79. package/src/_modules/socket/_models/socket-server-service-params.control-model.spec.ts +23 -0
  80. package/src/_modules/socket/_models/socket-server-service-params.control-model.ts +12 -1
  81. package/src/_modules/socket/_services/socket-server.service.ts +2 -0
  82. package/src/_modules/socket/app-extended.server.ts +15 -9
  83. package/src/_services/base/db.service.ts +39 -1
  84. package/src/_services/core/api.service.spec.ts +47 -0
  85. package/src/_services/core/api.service.ts +32 -12
  86. package/src/_services/core/db-stats.service.ts +104 -0
  87. package/src/_services/core/diagnostics.service.spec.ts +44 -0
  88. package/src/_services/core/diagnostics.service.ts +158 -0
  89. package/src/_services/core/migration-runner.service.spec.ts +91 -0
  90. package/src/_services/core/migration-runner.service.ts +106 -0
  91. package/src/index.ts +7 -0
@@ -261,6 +261,18 @@ export class DyNTS_Endpoint_Params<
261
261
  return;
262
262
  }
263
263
 
264
+ // BFR-OVERSEER-010: a logResponseContent IMPLEMENTÁLVA (a korábbi "not implemented yet"
265
+ // warn-stub minden requestre spam-elt). A res.send-et wrap-eljük a küldött body
266
+ // elkapásához; a task-ok után csonkolt preview-t logolunk (log-flood ellen).
267
+ let capturedResponseBody: unknown;
268
+ if (this.logRequest && this.logResponseContent) {
269
+ const originalSend: Response['send'] = res.send.bind(res);
270
+ res.send = (body?: unknown): Response => {
271
+ capturedResponseBody = body;
272
+ return originalSend(body);
273
+ };
274
+ }
275
+
264
276
  await DyFM_Array.asyncForEach(this.tasks,
265
277
  async (
266
278
  task: (req: Request, res: Response, issuer?: string) => Promise<void>
@@ -268,13 +280,11 @@ export class DyNTS_Endpoint_Params<
268
280
  await task(req, res, issuer);
269
281
  }
270
282
  );
271
-
283
+
272
284
  if (this.logRequest) {
285
+ DyFM_Log.success(` <<<===== '${this.name}' result sent.`);
273
286
  if (this.logResponseContent) {
274
- DyFM_Log.success(` <<<===== '${this.name}' result sent.`);
275
- DyFM_Log.warn('sorry, the logResponseContent is not implemented yet.');
276
- } else {
277
- DyFM_Log.success(` <<<===== '${this.name}' result sent.`);
287
+ DyFM_Log.info(` response: ${DyNTS_Endpoint_Params.previewBody(capturedResponseBody)}`);
278
288
  }
279
289
  }
280
290
  } catch (error) {
@@ -283,6 +293,28 @@ export class DyNTS_Endpoint_Params<
283
293
  };
284
294
  }
285
295
 
296
+ /**
297
+ * BFR-OVERSEER-010: csonkolt, safe response-preview a logResponseContent-hez —
298
+ * nem-parseolható/circular body-ra nem dob, hosszú body-t levág (log-flood ellen).
299
+ */
300
+ private static previewBody(body: unknown): string {
301
+ const maxLength: number = 2000;
302
+ let text: string;
303
+ if (body === undefined) {
304
+ return '-no body captured-';
305
+ }
306
+ if (typeof body === 'string') {
307
+ text = body;
308
+ } else {
309
+ try {
310
+ text = JSON.stringify(body);
311
+ } catch {
312
+ text = String(body);
313
+ }
314
+ }
315
+ return text.length > maxLength ? `${text.slice(0, maxLength)}…(truncated ${text.length} chars)` : text;
316
+ }
317
+
286
318
  /**
287
319
  *
288
320
  * @param res
@@ -492,20 +524,16 @@ export class DyNTS_Endpoint_Params<
492
524
  // test/dev környezetben TELJES DyFM-leaket hagyott (__localStack + ___systemVersion + issuer).
493
525
  // A szabályok (mindig-strip / prod-only additionalContent / kliens-mezők megtartása) SSOT-ja
494
526
  // a DyNTS_ClientSafeError_Util.
495
- res.send(DyNTS_ClientSafeError_Util.toClientSafe(error));
527
+ // BFR-OVERSEER-010: a kiküldött client-safe error megőrzése a logResponseContent-hez.
528
+ const clientSafeError: unknown = DyNTS_ClientSafeError_Util.toClientSafe(error);
529
+ res.send(clientSafeError);
496
530
 
497
531
  if (this.logRequest) {
532
+ DyFM_Log.error(
533
+ ` <<<===== '${this.name}' error sent: ${(error as DyFM_Error)?._message ?? ''}`
534
+ );
498
535
  if (this.logResponseContent) {
499
- DyFM_Log.error(
500
- ` <<<===== '${this.name}' error sent: ${(error as DyFM_Error)?._message ?? ''}`
501
- );
502
- DyFM_Log.error(
503
- 'sorry, the logResponseContent is not implemented yet.'
504
- );
505
- } else {
506
- DyFM_Log.error(
507
- ` <<<===== '${this.name}' error sent: ${(error as DyFM_Error)?._message ?? ''}`
508
- );
536
+ DyFM_Log.error(` error response: ${DyNTS_Endpoint_Params.previewBody(clientSafeError)}`);
509
537
  }
510
538
  }
511
539
 
@@ -0,0 +1,50 @@
1
+ import { DyFM_DataModel_Params, DyFM_Metadata } from '@futdevpro/fsm-dynamo';
2
+
3
+ /**
4
+ * **Migráció-nyilvántartó ("server info") LEDGER** — bedrock, újrahasználható az egész flottának.
5
+ * (A token-service app-lokál `SchemaMigration` mintájából emelve bedrock nts-be.)
6
+ *
7
+ * Egy-egy `migrationId` **PONTOSAN EGYSZER** fut le rendszerenként: a `DyNTS_Migration_Runner.runPending()`
8
+ * boot-on (az app `app.server.postProcess()`-éből) lekéri a már-alkalmazott ID-kat, a pending
9
+ * migrációkat lefuttatja, majd egy sort rögzít ide. A `migrationId` **unique index** → race-safe
10
+ * (két párhuzamos instance esetén az egyik `save`-je duplicate-key-re bukik → skip).
11
+ *
12
+ * A ledger a KÖTELEZŐ kanon-adatokat tartja: **mely migrációk futottak** (id + idő) + **mely app-verzió**
13
+ * alkalmazta (a `appliedVersion`-ökből a "mely verziók futottak" levezethető).
14
+ *
15
+ * Az app a saját DB-jében regisztrálja a `dyNTS_schemaMigration_dataParams`-t (`getGlobalServiceCollection
16
+ * ().dbModels`), így per-app saját `dynts_schema_migrations` collection-t kap.
17
+ */
18
+ export class DyNTS_SchemaMigration extends DyFM_Metadata {
19
+
20
+ /** A migráció egyedi azonosítója (konvenció: `YYYY-MM-DD-NNN-descriptor`). */
21
+ migrationId?: string;
22
+
23
+ /** Alkalmazás időbélyege. */
24
+ appliedAt?: Date;
25
+
26
+ /** Az alkalmazó app-verzió (a `package.json` version-je — audit-trail + "mely verziók futottak"). */
27
+ appliedVersion?: string;
28
+
29
+ /** Diag — pl. `touched=<n>` vagy hiba-részletek. */
30
+ notes?: string;
31
+
32
+ constructor(set?: DyNTS_SchemaMigration) {
33
+ super(set);
34
+ if (set) {
35
+ Object.assign(this, set);
36
+ }
37
+ }
38
+ }
39
+
40
+ export const dyNTS_schemaMigration_dataParams: DyFM_DataModel_Params<DyNTS_SchemaMigration> =
41
+ new DyFM_DataModel_Params<DyNTS_SchemaMigration>({
42
+ dataName: 'dynts_schema_migrations',
43
+ addArchive: false,
44
+ properties: {
45
+ migrationId: { type: 'string', required: true, unique: true, index: true },
46
+ appliedAt: { type: 'date', required: true },
47
+ appliedVersion: { type: 'string' },
48
+ notes: { type: 'string' },
49
+ },
50
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Egy regisztrált deploy-integrált migráció leírója. Az app egy `DyNTS_MigrationEntry[]` registry-t
3
+ * ad át a `DyNTS_Migration_Runner.runPending(...)`-nek; a runner a pending (még nem alkalmazott)
4
+ * entry-ket **egyszer** futtatja le, sorrendben, majd a `DyNTS_SchemaMigration` ledgerbe rögzíti.
5
+ */
6
+ export interface DyNTS_MigrationEntry {
7
+ /** Egyedi, monoton bővíthető ID (konvenció: `YYYY-MM-DD-NNN-descriptor`). A ledger ez alapján run-once-ol. */
8
+ id: string;
9
+
10
+ /** Rövid leíró (admin/debug/log). */
11
+ description: string;
12
+
13
+ /**
14
+ * A tényleges migráció-logika. Az app már-csatlakozott Mongo-kapcsolatát használja (a runner a
15
+ * `postProcess`-ből fut, ahol a DB már él). Hibára DOBHAT — a runner catch-eli, a `notes`-ba menti,
16
+ * és NEM állítja meg a boot-ot. Visszaad: `{ touched }` (a módosított dokumentumok száma — count/riport).
17
+ */
18
+ run: (issuer: string) => Promise<{ touched: number }>;
19
+ }
@@ -75,6 +75,22 @@ describe('| DyNTS_LVS_PersistentVectorPool_ControlService', () => {
75
75
  (collectionKey: string) =>
76
76
  Promise.resolve(Array.from(store.get(collectionKey)?.values() ?? [])),
77
77
  );
78
+
79
+ // BFR-AM-013 (hydration-paging): a batch-elt lean-scan fake-je — a fake-store tartalmát
80
+ // batch-enként adagolja a handler-nek (a valós cursor-viselkedés memóriában modellezve).
81
+ spyOn(DyNTS_LVS_VectorPersist_DataService.prototype, 'findLeanBatchedByCollectionKey').and.callFake(
82
+ async (
83
+ collectionKey: string,
84
+ set: { batchSize?: number; handler: (batch: Partial<DyNTS_LVS_VectorPersist>[]) => Promise<void> | void },
85
+ ): Promise<number> => {
86
+ const all: DyNTS_LVS_VectorPersist[] = Array.from(store.get(collectionKey)?.values() ?? []);
87
+ const batchSize: number = set.batchSize && set.batchSize > 0 ? set.batchSize : 500;
88
+ for (let i: number = 0; i < all.length; i += batchSize) {
89
+ await set.handler(all.slice(i, i + batchSize));
90
+ }
91
+ return all.length;
92
+ },
93
+ );
78
94
  });
79
95
 
80
96
  /** Egy egységvektor a tengely-irányba (cosine-self = 1) — determinisztikus keresés-ellenőrzéshez. */
@@ -1,5 +1,5 @@
1
1
 
2
- import { DyFM_Log } from '@futdevpro/fsm-dynamo';
2
+ import { DyFM_Error, DyFM_Log } from '@futdevpro/fsm-dynamo';
3
3
 
4
4
  import { LVS_Search_Mode } from '../_enums/lvs-search-mode.enum';
5
5
  import { LVS_SearchResult } from '../_models/lvs-search-result.interface';
@@ -97,24 +97,60 @@ export class DyNTS_LVS_PersistentVectorPool_ControlService {
97
97
  * előbb üríti (idempotens hidratálás). A hiányos (üres `vectorId`/`embedding`) rekordokat átugorja.
98
98
  * Visszaadja a betöltött vektorok számát. **NEM** Atlas — saját Mongo + in-memory pool.
99
99
  */
100
- async hydrateFromMongo(): Promise<number> {
101
- const records: DyNTS_LVS_VectorPersist[] =
102
- await this.getPersistService().findByCollectionKey(this.collectionKey);
103
-
100
+ async hydrateFromMongo(set?: {
101
+ /** Cursor-batch méret (default 500) — a heap-tranziens felső korlátja egy batch. */
102
+ batchSize?: number,
103
+ /** Opcionális felső korlát a pool-méretre; elérésekor a hidratálás áll + WARN (no-silent-cap). */
104
+ maxVectors?: number,
105
+ }): Promise<number> {
106
+ // BFR-AM-013 (hydration-paging): a korábbi impl a TELJES collection-t egyetlen
107
+ // `findByCollectionKey`-jel (Mongoose-doc-tömb) töltötte memóriába → 32k×2560-dim vektornál
108
+ // RUNTIME-IGAZOLT 93% heap-spike (OOM-közeli). Mostantól LEAN + projektált (vectorId+embedding,
109
+ // a content-szöveg NÉLKÜL) CURSOR-batch — a tranziens felső korlátja EGY batch.
104
110
  this.pool.clearPool();
105
111
 
106
112
  let loaded: number = 0;
107
-
108
- for (const record of records) {
109
- if (!record.vectorId || !record.embedding?.length) {
110
- continue;
111
- }
112
- this.pool.addVector(record.vectorId, record.embedding);
113
- loaded++;
114
- }
113
+ let skipped: number = 0;
114
+ let truncated: boolean = false;
115
+
116
+ await this.getPersistService().findLeanBatchedByCollectionKey(this.collectionKey, {
117
+ batchSize: set?.batchSize,
118
+ handler: (batch: Partial<DyNTS_LVS_VectorPersist>[]): void => {
119
+ for (const record of batch) {
120
+ if (truncated) {
121
+ return;
122
+ }
123
+ if (set?.maxVectors !== undefined && loaded >= set.maxVectors) {
124
+ truncated = true;
125
+ DyFM_Log.warn(
126
+ `[DyNTS LVS hydrate] '${this.collectionKey}': maxVectors (${set.maxVectors}) elérve — ` +
127
+ 'a hidratálás CSONKOLVA (no-silent-cap; a maradék vektor NEM került a poolba).',
128
+ );
129
+ return;
130
+ }
131
+ if (!record.vectorId || !record.embedding?.length) {
132
+ continue;
133
+ }
134
+ try {
135
+ this.pool.addVector(record.vectorId, record.embedding);
136
+ loaded++;
137
+ } catch (error) {
138
+ // BFR-AM-013 (NaN/±Inf-guard a hidratálásban): EGY hibás (nem-véges elemű) perzistált
139
+ // vektor NEM boríthatja a teljes boot-hidratálást — skip + warn (nem néma).
140
+ skipped++;
141
+ DyFM_Log.warn(
142
+ `[DyNTS LVS hydrate] '${this.collectionKey}': vektor kihagyva ("${record.vectorId}") — ` +
143
+ `${DyFM_Error.getErrorMessage(error)}`,
144
+ );
145
+ }
146
+ }
147
+ },
148
+ });
115
149
 
116
150
  DyFM_Log.log(
117
- `[DyNTS LVS hydrate] '${this.collectionKey}' pool: ${loaded} vektor betöltve az in-memory pool-ba.`,
151
+ `[DyNTS LVS hydrate] '${this.collectionKey}' pool: ${loaded} vektor betöltve az in-memory pool-ba` +
152
+ (skipped ? ` (${skipped} hibás vektor kihagyva)` : '') +
153
+ (truncated ? ' (CSONKOLT — maxVectors)' : '') + '.',
118
154
  );
119
155
 
120
156
  return loaded;
@@ -48,6 +48,28 @@ export class DyNTS_LVS_VectorPersist_DataService extends DyNTS_DataService<DyNTS
48
48
  );
49
49
  }
50
50
 
51
+ /**
52
+ * BFR-AM-013 (hydration-paging): LEAN + projektált (CSAK `vectorId` + `embedding` — a content-szöveg
53
+ * és metadata NÉLKÜL) cursor-batch scan a boot-hidratáláshoz. A heap-tranziens felső korlátja EGY batch
54
+ * (a teljes result-set + Mongoose-doc-boxing kimarad). Soft-delete-elt rekordok kihagyva.
55
+ */
56
+ async findLeanBatchedByCollectionKey(
57
+ collectionKey: string,
58
+ set: {
59
+ batchSize?: number,
60
+ handler: (batch: Partial<DyNTS_LVS_VectorPersist>[]) => Promise<void> | void,
61
+ }
62
+ ): Promise<number> {
63
+ return this.dataDBService.findLeanBatched(
64
+ { collectionKey: collectionKey, _deleted: undefined },
65
+ {
66
+ projection: { vectorId: 1, embedding: 1 },
67
+ batchSize: set.batchSize,
68
+ handler: set.handler,
69
+ }
70
+ );
71
+ }
72
+
51
73
  /**
52
74
  * Egy vektor ATOMIKUS upsert-je a `(collectionKey, vectorId)` logikai-kulcson. MEGLÉVŐ rekordnál
53
75
  * `$set` (a Mixed `metadata` silent-drop ellen — memory: mongoose_mixed_atomic_write); ÚJ rekordnál
@@ -374,9 +374,11 @@ describe('| LVS_VectorPool_ControlService', () => {
374
374
  expect(vec1Result!.score).toBeCloseTo(1, 10);
375
375
  });
376
376
 
377
- it('| should produce cosine scores IDENTICAL to static cosineSimilarity (precomputed-normalized perf optimization, BFR-AM-013)', () => {
377
+ it('| should produce cosine scores EQUIVALENT to static cosineSimilarity (precomputed-normalized + Float32 storage, BFR-AM-013)', () => {
378
378
  // Regression-guard: a search optimalizalt (precomputed-normalized + alloc-mentes) cosine-ja
379
- // viselkedes-identikus a regi per-osszehasonlitas `cosineSimilarity(query, vector)`-ral.
379
+ // viselkedes-EKVIVALENS a regi per-osszehasonlitas `cosineSimilarity(query, vector)`-ral.
380
+ // Tolerancia 6 tizedes (nem 10): a pool BELSŐ tárolása Float32Array (BFR-AM-013 Tier-1,
381
+ // ~2-4× memória) → ~1e-7 kerekítési eltérés — a similarity-RANGSORT nem érinti.
380
382
  const query: number[] = [0.3, 0.7, 0.2];
381
383
  const results: LVS_SearchResult[] = vectorPool.search(query, 4, LVS_Search_Mode.cosineSimilarity);
382
384
 
@@ -386,7 +388,7 @@ describe('| LVS_VectorPool_ControlService', () => {
386
388
  const original: number[] | undefined = originals.get(result.id);
387
389
  expect(original).toBeDefined();
388
390
  const expected: number = LVS_VectorPool_ControlService.cosineSimilarity(query, original!);
389
- expect(result.score).toBeCloseTo(expected, 10);
391
+ expect(result.score).toBeCloseTo(expected, 6);
390
392
  }
391
393
  });
392
394
 
@@ -1,3 +1,5 @@
1
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
2
+
1
3
  import { LVS_Search_Mode } from '../_enums/lvs-search-mode.enum';
2
4
  import { LVS_SearchResult } from '../_models/lvs-search-result.interface';
3
5
 
@@ -10,14 +12,19 @@ export class LVS_VectorPool_ControlService {
10
12
  /**
11
13
  * In-memory vektor pool tárolás
12
14
  * Key: vektor ID, Value: vektor értékek
15
+ *
16
+ * BFR-AM-013 (Tier-1 memória): a belső tárolás `Float32Array` (4 byte/elem, boxing nélkül) a
17
+ * korábbi `number[]` (8 byte + array-boxing) helyett → ~2–4× kisebb PERSISTENS pool-footprint
18
+ * (2560-dim × 32k vektornál a heap érdemi része). A cosine/l2 index-alapú loopjai változatlanul
19
+ * futnak rajta; a publikus `getAll()` kontraktja (number[]) konverzióval megőrzött.
13
20
  */
14
- private vectorPool: Map<string, number[]> = new Map<string, number[]>();
21
+ private vectorPool: Map<string, Float32Array> = new Map<string, Float32Array>();
15
22
 
16
23
  /**
17
24
  * Normalizált vektor pool tárolás (L2 normalizált másolatok)
18
- * Key: vektor ID, Value: L2 normalizált vektor értékek
25
+ * Key: vektor ID, Value: L2 normalizált vektor értékek (Float32Array — l. fent)
19
26
  */
20
- private normalizedVectorPool: Map<string, number[]> = new Map<string, number[]>();
27
+ private normalizedVectorPool: Map<string, Float32Array> = new Map<string, Float32Array>();
21
28
 
22
29
  /**
23
30
  * `normalizedOnly` mód: ha `true`, a pool NEM tárolja a nyers vektorokat (`vectorPool`) — CSAK a
@@ -52,13 +59,13 @@ export class LVS_VectorPool_ControlService {
52
59
  }
53
60
 
54
61
  // Tároljuk az eredeti vektort — KIVÉVE normalizedOnly módban (akkor a nyers pool kihagyva, ~50% memória).
62
+ // Float32Array-ként (BFR-AM-013 Tier-1) — a number[]-boxing nélkül.
55
63
  if (!this.normalizedOnly) {
56
- this.vectorPool.set(id, vector);
64
+ this.vectorPool.set(id, Float32Array.from(vector));
57
65
  }
58
66
 
59
67
  // L2 normalizált másolatot is tárolunk a cosine similarity számításokhoz
60
- const normalized: number[] = LVS_VectorPool_ControlService.l2Normalize(vector);
61
- this.normalizedVectorPool.set(id, normalized);
68
+ this.normalizedVectorPool.set(id, LVS_VectorPool_ControlService.l2NormalizeFloat32(vector));
62
69
  }
63
70
 
64
71
  /**
@@ -102,7 +109,33 @@ export class LVS_VectorPool_ControlService {
102
109
  * Debugging és export céljára
103
110
  */
104
111
  getAll(): Map<string, number[]> {
105
- return new Map<string, number[]>(this.vectorPool);
112
+ // A publikus kontrakt (number[]) megőrzése — a belső Float32Array-ből konvertálva.
113
+ const result: Map<string, number[]> = new Map<string, number[]>();
114
+ for (const [ id, vector ] of this.vectorPool.entries()) {
115
+ result.set(id, Array.from(vector));
116
+ }
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * BFR-AM-013 (Tier-1): L2-normalizálás Float32Array-be, explicit loopokkal (allokáció-minimális) —
122
+ * a pool BELSŐ tárolási formátumához. Nullvektor → csupa-0 Float32Array.
123
+ */
124
+ private static l2NormalizeFloat32(vector: ArrayLike<number>): Float32Array {
125
+ let sumSquares: number = 0;
126
+ for (let i: number = 0; i < vector.length; i++) {
127
+ sumSquares += vector[i] * vector[i];
128
+ }
129
+ const magnitude: number = Math.sqrt(sumSquares);
130
+
131
+ const normalized: Float32Array = new Float32Array(vector.length);
132
+ if (magnitude === 0) {
133
+ return normalized;
134
+ }
135
+ for (let i: number = 0; i < vector.length; i++) {
136
+ normalized[i] = vector[i] / magnitude;
137
+ }
138
+ return normalized;
106
139
  }
107
140
 
108
141
  /**
@@ -156,7 +189,7 @@ export class LVS_VectorPool_ControlService {
156
189
  * Eredmény: 0 vagy pozitív érték, ahol 0 a legkisebb távolság
157
190
  * Statikus metódus, mivel nincs szüksége instance state-re
158
191
  */
159
- static l2Distance(a: number[], b: number[]): number {
192
+ static l2Distance(a: ArrayLike<number>, b: ArrayLike<number>): number {
160
193
  if (a.length !== b.length) {
161
194
  throw new Error(
162
195
  `Vectors must have the same dimension. Got ${a.length} and ${b.length}`
@@ -200,6 +233,14 @@ export class LVS_VectorPool_ControlService {
200
233
  throw new Error('Query vector must be a non-empty array');
201
234
  }
202
235
 
236
+ // BFR-AM-013 (NaN/±Inf-guard): nem-véges query-elem NÉMÁN mérgezné a TELJES rangsort
237
+ // (minden dot-product NaN → a valódi találatok kiesnek, hiba nélkül) → fail-closed throw.
238
+ for (let i: number = 0; i < query.length; i++) {
239
+ if (!Number.isFinite(query[i])) {
240
+ throw new Error(`Query vector must contain only finite numbers (non-finite at index ${i})`);
241
+ }
242
+ }
243
+
203
244
  if (k <= 0) {
204
245
  throw new Error('k must be a positive number');
205
246
  }
@@ -217,9 +258,9 @@ export class LVS_VectorPool_ControlService {
217
258
 
218
259
  // Validáljuk, hogy minden vektor ugyanolyan dimenziójú-e. A referencia-pool mód-függő: l2 a nyers,
219
260
  // cosine a normalizált (azonos dimenzió, de `normalizedOnly`-ban a nyers pool üres).
220
- const referencePool: Map<string, number[]> =
261
+ const referencePool: Map<string, Float32Array> =
221
262
  mode === LVS_Search_Mode.l2Distance ? this.vectorPool : this.normalizedVectorPool;
222
- const firstVector: number[] | undefined = Array.from(referencePool.values())[0];
263
+ const firstVector: Float32Array | undefined = referencePool.values().next().value;
223
264
  if (firstVector && firstVector.length !== query.length) {
224
265
  throw new Error(
225
266
  `Query vector dimension (${query.length}) does not match pool vector ` +
@@ -239,7 +280,10 @@ export class LVS_VectorPool_ControlService {
239
280
  // (2) a per-összehasonlítás dot-product ALLOKÁCIÓ-MENTES (a régi `l2Normalize` hívásonként KÉT új
240
281
  // tömböt allokált → egy 24k-vektoros keresés ~48k tömb-allokáció + GC-spike-ot okozott).
241
282
  // A drágán eltárolt `normalizedVectorPool` korábban SOHA nem volt használva a keresésben.
242
- const queryNormalized: number[] = LVS_VectorPool_ControlService.l2Normalize(query);
283
+ const queryNormalized: Float32Array = LVS_VectorPool_ControlService.l2NormalizeFloat32(query);
284
+ // BFR-AM-013 (NaN-guard, defensive): ha egy tárolt vektor mégis NaN-score-t adna (elvileg az
285
+ // addVector-guard kizárja), a találat KIESIK — nem mérgezi a rangsort — és warn-t adunk (nem néma).
286
+ let nanScoreCount: number = 0;
243
287
  for (const [id, normalizedVector] of this.normalizedVectorPool.entries()) {
244
288
  if (candidateIds && !candidateIds.has(id)) {
245
289
  continue;
@@ -248,8 +292,15 @@ export class LVS_VectorPool_ControlService {
248
292
  for (let i: number = 0; i < queryNormalized.length; i++) {
249
293
  dotProduct += queryNormalized[i] * normalizedVector[i];
250
294
  }
295
+ if (!Number.isFinite(dotProduct)) {
296
+ nanScoreCount++;
297
+ continue;
298
+ }
251
299
  results.push({ id, score: dotProduct });
252
300
  }
301
+ if (nanScoreCount) {
302
+ DyFM_Log.warn(`[LVS search] ${nanScoreCount} vector(s) produced non-finite scores and were skipped.`);
303
+ }
253
304
  // Cosine similarity: csökkenő sorrend (legnagyobb hasonlóság először)
254
305
  results.sort((a: LVS_SearchResult, b: LVS_SearchResult) => b.score - a.score);
255
306
  } else if (mode === LVS_Search_Mode.l2Distance) {
@@ -14,6 +14,8 @@ import * as mongoose from 'mongoose';
14
14
 
15
15
  import { version as _serverVersion } from '../../../../package.json';
16
16
  import { DyNTS_SingletonService } from '../../../_services/base/singleton.service';
17
+ import { DyNTS_DbStats, DyNTS_DbStats_Util } from '../../../_services/core/db-stats.service';
18
+ import { DyNTS_Diagnostics, DyNTS_DoctorReport } from '../../../_services/core/diagnostics.service';
17
19
  import { DyNTS_global_settings } from '../../../_collections/global-settings.const';
18
20
  import { DyNTS_Errors_ControlService } from '../errors/errors.control-service';
19
21
  import { DyNTS_ServerStatusSnapshot_ControlService } from './server-status-snapshot.control-service';
@@ -221,6 +223,16 @@ export abstract class DyNTS_ServerStatus_ControlService<
221
223
  * A mongoose default-connection. Külön metódus = teszt-seam (spy-jal felülírható, hogy a
222
224
  * {@link checkDbReadiness} DB nélkül is tesztelhető legyen).
223
225
  */
226
+ /** BFR-OVERSEER-004 — stats-only DB-diagnosztika a `/server/db-stats` endpointnak (metaadat-only). */
227
+ async getDbStats(): Promise<DyNTS_DbStats> {
228
+ return DyNTS_DbStats_Util.collect(this.getMongooseConnection());
229
+ }
230
+
231
+ /** BFR-AM-015 — aggregált doctor-riport a `/server/doctor` endpointnak (sosem dob). */
232
+ async runDoctor(): Promise<DyNTS_DoctorReport> {
233
+ return DyNTS_Diagnostics.runDoctor({ connection: this.getMongooseConnection() });
234
+ }
235
+
224
236
  protected getMongooseConnection(): mongoose.Connection {
225
237
  // FIX (2026-07-22) — a `mongoose.connection` NEM feltetlenul az a handle, amit a data-layer hasznal.
226
238
  //
@@ -252,6 +252,34 @@ export abstract class DyNTS_ServerStatus_Controller<
252
252
  ],
253
253
  }),
254
254
 
255
+ // BFR-OVERSEER-004 — stats-only DB-diagnosztika (per-collection count + méret + index-méret;
256
+ // metaadat-only, SOHA nem dokumentum-tartalom). App-auth-tal gate-elhető (lazyPreProcessesFor).
257
+ new DyNTS_Endpoint_Params({
258
+ name: 'getDbStats',
259
+ type: DyFM_HttpCallType.get,
260
+ endpoint: '/db-stats',
261
+ preProcesses: this.lazyPreProcessesFor('getDbStats'),
262
+ tasks: [
263
+ async (req: Request, res: Response): Promise<void> => {
264
+ res.send(await this.server_CS.getDbStats());
265
+ },
266
+ ],
267
+ }),
268
+
269
+ // BFR-AM-015 — aggregált doctor (mongo-probe + memory + event-loop + db-stats + app-checkek;
270
+ // health: ok|warn|fail + per-check fixHint). App-auth-tal gate-elhető.
271
+ new DyNTS_Endpoint_Params({
272
+ name: 'getDoctor',
273
+ type: DyFM_HttpCallType.get,
274
+ endpoint: '/doctor',
275
+ preProcesses: this.lazyPreProcessesFor('getDoctor'),
276
+ tasks: [
277
+ async (req: Request, res: Response): Promise<void> => {
278
+ res.send(await this.server_CS.runDoctor());
279
+ },
280
+ ],
281
+ }),
282
+
255
283
  // Readiness-probe: a /health-tel (liveness) szemben a DB-elérhetőséget is ellenőrzi.
256
284
  // Public (mint a /health) — orchestrátor/LB hívja; 503 ha a DB nem ready → no traffic.
257
285
  new DyNTS_Endpoint_Params({
@@ -43,4 +43,27 @@ describe('| DyNTS_SocketServerService_Params', () => {
43
43
  expect(instance.port).toBeUndefined();
44
44
  expect(instance.security).toBe(DyNTS_SocketSecurity.open);
45
45
  });
46
+
47
+ // BFR-MASTERPROMPTER-025 — az attach-mód EXPLICIT opt-in, a port önmagában NEM diszkriminátor
48
+ it('| dedicatedPort default FALSE (shared) — port-label önmagában nem vált dedikáltra', () => {
49
+ const instance = new DyNTS_SocketServerService_Params({
50
+ name: 'LegacyPortLabel',
51
+ port: 39047,
52
+ security: DyNTS_SocketSecurity.open,
53
+ });
54
+
55
+ expect(instance.dedicatedPort).toBe(false);
56
+ });
57
+
58
+ it('| dedicatedPort: true explicit opt-in megőrződik', () => {
59
+ const instance = new DyNTS_SocketServerService_Params({
60
+ name: 'DedicatedNotif',
61
+ port: 39157,
62
+ dedicatedPort: true,
63
+ security: DyNTS_SocketSecurity.open,
64
+ });
65
+
66
+ expect(instance.dedicatedPort).toBe(true);
67
+ expect(instance.port).toBe(39157);
68
+ });
46
69
  });
@@ -4,12 +4,22 @@ import { DyNTS_SocketSecurity } from '../_enums/socket-security.enum';
4
4
 
5
5
  /**
6
6
  * Socket service paraméterek.
7
- * A port opcionális: ha nincs megadva, az app-extended a getPortSettings() httpPort/httpsPort értékét használja (ugyanaz a port, mint a HTTP/HTTPS).
7
+ *
8
+ * **BFR-MASTERPROMPTER-025 (attach-mód):** a socket alapból a fő HTTP/HTTPS szerverre attacholódik
9
+ * (SHARED — unified-port modell). Dedikált saját listener CSAK explicit `dedicatedPort: true`-val kérhető
10
+ * (+ `port` kötelező és ≠ fő port). A korábbi implicit inferencia (port≠httpPort → dedikált) csendben
11
+ * regresszálta azokat a unified-appokat, amelyek legacy-label okból adtak portot — ezért az attach-mód
12
+ * mostantól KÓD-EXPLICIT, a `port` önmagában NEM diszkriminátor.
8
13
  */
9
14
  export class DyNTS_SocketServerService_Params<T = any> {
10
15
  name: string;
11
16
  /** Opcionális: ha nincs megadva, az extended app a HTTP/HTTPS portot használja (same port). */
12
17
  port?: number;
18
+ /**
19
+ * EXPLICIT opt-in a dedikált saját listenerre (a fő szerver HELYETT). Default: false = shared.
20
+ * `true` esetén a `port` kötelező és el kell térnie a fő HTTP-porttól.
21
+ */
22
+ dedicatedPort?: boolean;
13
23
  security?: DyNTS_SocketSecurity;
14
24
 
15
25
  constructor(
@@ -17,6 +27,7 @@ export class DyNTS_SocketServerService_Params<T = any> {
17
27
  ) {
18
28
  this.name = set.name;
19
29
  this.port = set.port;
30
+ this.dedicatedPort = set.dedicatedPort ?? false;
20
31
  this.security = set.security ? set.security : DyNTS_global_settings.defaultSocketSecurity;
21
32
  }
22
33
  }
@@ -51,6 +51,8 @@ export abstract class DyNTS_SocketServerService<
51
51
  protected params: DyNTS_SocketServerService_Params;
52
52
  get name(): string { return this.params.name; }
53
53
  get port(): number | undefined { return this.params.port; }
54
+ /** BFR-MASTERPROMPTER-025: explicit dedikált-listener opt-in (default false = shared). */
55
+ get dedicatedPort(): boolean { return this.params.dedicatedPort ?? false; }
54
56
  get security(): DyNTS_SocketSecurity { return this.params.security; }
55
57
  get activeSubscriptionsCount(): number { return this.presences.length; }
56
58
 
@@ -512,15 +512,21 @@ export abstract class DyNTS_AppExtended extends DyNTS_App {
512
512
  if (service.security === DyNTS_SocketSecurity.open) {
513
513
  this.systemControlsExt.httpSocketServer.init = true;
514
514
 
515
- // BFR-FDPTOKENSERVICE-007 (fleet-wide realtime 502): ha a service SAJÁT (dedikált) portot
516
- // kér, ami ELTÉR a fő HTTP-porttól, a socket a SAJÁT portjára listen-el — NEM a fő szerverre
517
- // attacholódik. Enélkül base-server mellett a dedikált notif-port SOSEM kap listenert (a
518
- // socket a fő portra attacholt) → a gateway a notif-porton 502-t ad. A fő-portot MEGOSZTÓ
519
- // socket (port nincs, v. == httpPort) viselkedése VÁLTOZATLAN. A dedikált-ág a már bizonyított
520
- // non-base útvonalat tükrözi (saját Http.Server + listen).
521
- const sharesMainHttpPort: boolean =
522
- service.port === undefined || service.port === this.portSettings.httpPort;
523
- const attachToMain: boolean = useBaseHttpServer && sharesMainHttpPort;
515
+ // BFR-MASTERPROMPTER-025 (a BFR-FDPTOKENSERVICE-007 3a2c381 utó-fix-e): az attach-mód
516
+ // EXPLICIT opt-in (`dedicatedPort: true`), NEM a port-érték implicit inferenciája. A
517
+ // korábbi `port≠httpPort dedikált` inferencia CSENDBEN regresszálta a unified-shared
518
+ // appokat, amelyek legacy-label/notif-konvenció okból adtak portot (MP live-igazolt:
519
+ // realtime DEAD az nts-bump után; adventor/ccap/warbots latens). Mostantól:
520
+ // - default (dedicatedPort hiányzik/false) → SHARED (a szerverre attach, a port-label irreleváns);
521
+ // - dedicatedPort: true → saját Http.Server + listen a service.port-on (a bizonyított
522
+ // non-base útvonal tükre) pl. a token-service szándékosan-dedikált notif-portja.
523
+ if (service.dedicatedPort && (service.port === undefined || service.port === this.portSettings.httpPort)) {
524
+ throw new Error(
525
+ `SOCKET CONFIG ERROR: "${service.name}" dedicatedPort:true requires an explicit ` +
526
+ `port that differs from the main httpPort (${this.portSettings.httpPort}).`
527
+ );
528
+ }
529
+ const attachToMain: boolean = useBaseHttpServer && !service.dedicatedPort;
524
530
  const serverForOpen: Http.Server = attachToMain
525
531
  ? this.httpServer
526
532
  : (useBaseHttpServer