@futdevpro/nts-dynamo 1.15.60 → 1.15.64

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 (26) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1551 -1541
  2. package/.dynamo/logs/cicd-pipeline/status.json +39 -39
  3. package/.github/workflows/main.yml +432 -426
  4. package/build/_collections/global-settings.const.d.ts.map +1 -1
  5. package/build/_collections/global-settings.const.js +6 -0
  6. package/build/_collections/global-settings.const.js.map +1 -1
  7. package/build/_collections/mongo-reconnect-guard.util.d.ts +74 -0
  8. package/build/_collections/mongo-reconnect-guard.util.d.ts.map +1 -0
  9. package/build/_collections/mongo-reconnect-guard.util.js +111 -0
  10. package/build/_collections/mongo-reconnect-guard.util.js.map +1 -0
  11. package/build/_models/interfaces/global-settings.interface.d.ts +21 -0
  12. package/build/_models/interfaces/global-settings.interface.d.ts.map +1 -1
  13. package/build/_services/core/global.service.d.ts.map +1 -1
  14. package/build/_services/core/global.service.js +15 -2
  15. package/build/_services/core/global.service.js.map +1 -1
  16. package/build/_services/server/app.server.d.ts.map +1 -1
  17. package/build/_services/server/app.server.js +21 -0
  18. package/build/_services/server/app.server.js.map +1 -1
  19. package/package.json +1 -1
  20. package/src/_collections/global-settings.const.ts +7 -0
  21. package/src/_collections/mongo-reconnect-guard.util.spec.ts +52 -0
  22. package/src/_collections/mongo-reconnect-guard.util.ts +172 -0
  23. package/src/_models/interfaces/global-settings.interface.ts +22 -0
  24. package/src/_services/core/global.service.spec.ts +17 -0
  25. package/src/_services/core/global.service.ts +19 -5
  26. package/src/_services/server/app.server.ts +22 -1
@@ -54,6 +54,13 @@ export const DyNTS_global_settings: DyNTS_Global_Settings = {
54
54
 
55
55
  autoResearchIssues: true,
56
56
 
57
+ // Auth-service kötelező alapból (régi viselkedés). Public / no-auth szervereknél
58
+ // `auth_settings.optional = true` → getAuthService() undefined-ot ad dobás helyett,
59
+ // és a startup 'Authentication Service missing!' warn is elmarad.
60
+ auth_settings: {
61
+ optional: false,
62
+ },
63
+
57
64
  bot_settings: null,
58
65
 
59
66
  openAi_settings: null,
@@ -0,0 +1,52 @@
1
+ import { shouldForceMongoReconnect, MongoReconnectState } from './mongo-reconnect-guard.util';
2
+
3
+ /**
4
+ * A Mongo reconnect-guard PURE döntés-magjának unit-tesztjei. A helyesség kritikus, mert ez dönti
5
+ * el, hogy egy futó service force-reconnectolja-e a Mongo-kapcsolatát (disconnect+connect →
6
+ * re-resolve). Healthy connection-t SOHA nem szabad bántani; csak tartós nem-connected + grace +
7
+ * cooldown letelt esetén szabad force-reconnectolni.
8
+ */
9
+ describe('| mongo-reconnect-guard shouldForceMongoReconnect', () => {
10
+ const base: MongoReconnectState = {
11
+ readyState: 0,
12
+ msSinceUnhealthy: 60_000, // 60s unhealthy (a 30s grace fölött)
13
+ unhealthyGraceMs: 30_000,
14
+ reconnectInFlight: false,
15
+ msSinceLastReconnect: Number.POSITIVE_INFINITY,
16
+ reconnectCooldownMs: 20_000,
17
+ };
18
+
19
+ it('| connected (readyState=1) → SOHA nem reconnect (a healthy connection-t nem bántjuk)', () => {
20
+ expect(shouldForceMongoReconnect({ ...base, readyState: 1 }).reconnect).toBe(false);
21
+ });
22
+
23
+ it('| connecting (readyState=2) → nem reconnect (hagyjuk a drivert befejezni)', () => {
24
+ expect(shouldForceMongoReconnect({ ...base, readyState: 2 }).reconnect).toBe(false);
25
+ });
26
+
27
+ it('| disconnected + grace + cooldown letelt + nincs in-flight → RECONNECT', () => {
28
+ const d = shouldForceMongoReconnect({ ...base, readyState: 0 });
29
+ expect(d.reconnect).toBe(true);
30
+ expect(d.reason).toContain('re-resolve');
31
+ });
32
+
33
+ it('| disconnecting (readyState=3) ugyanígy → RECONNECT (tartós nem-connected)', () => {
34
+ expect(shouldForceMongoReconnect({ ...base, readyState: 3 }).reconnect).toBe(true);
35
+ });
36
+
37
+ it('| grace-en BELÜL → nem reconnect (hagyjuk a driver saját reconnect-jét előbb)', () => {
38
+ expect(shouldForceMongoReconnect({ ...base, msSinceUnhealthy: 10_000 }).reconnect).toBe(false);
39
+ });
40
+
41
+ it('| cooldown-on belül (frissen reconnectoltunk) → nem reconnect (anti-thrash)', () => {
42
+ expect(shouldForceMongoReconnect({ ...base, msSinceLastReconnect: 5_000 }).reconnect).toBe(false);
43
+ });
44
+
45
+ it('| már van in-flight reconnect → nem indítunk másikat', () => {
46
+ expect(shouldForceMongoReconnect({ ...base, reconnectInFlight: true }).reconnect).toBe(false);
47
+ });
48
+
49
+ it('| pontosan a grace-küszöbön (msSinceUnhealthy === grace) → RECONNECT', () => {
50
+ expect(shouldForceMongoReconnect({ ...base, msSinceUnhealthy: 30_000, unhealthyGraceMs: 30_000 }).reconnect).toBe(true);
51
+ });
52
+ });
@@ -0,0 +1,172 @@
1
+ import { DyFM_Log, second } from '@futdevpro/fsm-dynamo';
2
+
3
+ /**
4
+ * DyNTS Mongo reconnect-guard (2026-06-20) — TÚLÉLI a Mongo-konténer IP-VÁLTÁSÁT.
5
+ *
6
+ * PROBLÉMA (élőben, 2026-06-19): a MongoDB-driver a hostnevet (`mongodb`) connect-kor EGYSZER
7
+ * resolválja és cache-eli az IP-t (pl. 172.18.0.20); standalone-nál disconnect után UGYANAZT az
8
+ * IP-t pingeli, NEM re-resolve-ol. Ha a Mongo-konténert recreate-elik (új docker-network IP), a
9
+ * driver örökre a HALOTT IP-n lóg → `connect ECONNREFUSED <oldIp>:29017` + `buffering timed out`,
10
+ * és a service magától SOHA nem áll vissza (csak külső restart-tal). Ez a 2026-06-19 incidens fél
11
+ * test-stacket levitt (auth/ftp/helocia/futdevpro/… mind a régi Mongo-IP-n ragadt).
12
+ *
13
+ * MEGOLDÁS: a guard figyeli a `mongoose.connection.readyState`-et; ha TARTÓSAN nem-connected
14
+ * (a driver saját reconnect-grace-e UTÁN is), **TELJES `disconnect()` + `connect()`-et** csinál
15
+ * → ÚJ MongoClient → ÚJ DNS-resolve → új IP → reconnect. Így minden DyNTS_App-service magától
16
+ * túléli a Mongo IP-váltását, KÜLSŐ beavatkozás nélkül. Best-effort, sose dob; csak konténer-IP-
17
+ * frissítés, NEM törli/írja az adatot.
18
+ *
19
+ * SAFE always-on: egy app újracsatlakozása a SAJÁT DB-jéhez korrekt, nem-destruktív viselkedés
20
+ * (nincs az infra-restart-érzékenység, ami a külső watchdog-oknál — FR-229/237). Csak akkor
21
+ * avatkozik be, ha readyState !== 1 a grace + cooldown letelte UTÁN is; healthy connection-t (===1)
22
+ * SOHA nem bánt.
23
+ *
24
+ * A döntés-mag (`shouldForceMongoReconnect`) PURE + unit-tesztelt; a futtató injektált dep-ekkel
25
+ * dolgozik (readyState-olvasás / reconnect / log) a tesztelhetőségért.
26
+ */
27
+
28
+ /** A pure döntés inputja (minden numerikus → determinisztikus + tesztelhető). */
29
+ export interface MongoReconnectState {
30
+ /** mongoose.connection.readyState: 0=disconnected, 1=connected, 2=connecting, 3=disconnecting. */
31
+ readyState: number;
32
+ /** Mióta (ms) nem-connected a readyState (0, ha épp connected). */
33
+ msSinceUnhealthy: number;
34
+ /** Grace: ennyi ideig hagyjuk a drivert SAJÁT magát visszahozni, mielőtt force-reconnectolnánk. */
35
+ unhealthyGraceMs: number;
36
+ /** Épp folyamatban van-e már egy force-reconnect (ne torlódjon). */
37
+ reconnectInFlight: boolean;
38
+ /** Mióta (ms) volt az utolsó force-reconnect (Infinity, ha még nem volt). */
39
+ msSinceLastReconnect: number;
40
+ /** Két force-reconnect közti minimum (anti-thrash). */
41
+ reconnectCooldownMs: number;
42
+ }
43
+
44
+ /** A pure döntés eredménye. */
45
+ export interface MongoReconnectDecision {
46
+ reconnect: boolean;
47
+ reason: string;
48
+ }
49
+
50
+ /**
51
+ * PURE döntés: kell-e force-reconnect. Healthy (===1) / connecting (===2) / in-flight / grace-en
52
+ * vagy cooldown-on belül → NEM. Csak tartós nem-connected (disconnected/disconnecting) + grace és
53
+ * cooldown letelt + nincs in-flight → IGEN.
54
+ */
55
+ export function shouldForceMongoReconnect(s: MongoReconnectState): MongoReconnectDecision {
56
+ if (s.readyState === 1) {
57
+ return { reconnect: false, reason: 'connected (readyState=1)' };
58
+ }
59
+ if (s.readyState === 2) {
60
+ return { reconnect: false, reason: 'connecting (readyState=2) — letting the driver finish' };
61
+ }
62
+ if (s.reconnectInFlight) {
63
+ return { reconnect: false, reason: 'a force-reconnect is already in flight' };
64
+ }
65
+ if (s.msSinceUnhealthy < s.unhealthyGraceMs) {
66
+ return {
67
+ reconnect: false,
68
+ reason: `unhealthy for ${Math.round(s.msSinceUnhealthy / 1000)}s (< ${Math.round(s.unhealthyGraceMs / 1000)}s grace) — letting the driver self-heal first`,
69
+ };
70
+ }
71
+ if (s.msSinceLastReconnect < s.reconnectCooldownMs) {
72
+ return {
73
+ reconnect: false,
74
+ reason: `last force-reconnect ${Math.round(s.msSinceLastReconnect / 1000)}s ago (< ${Math.round(s.reconnectCooldownMs / 1000)}s cooldown)`,
75
+ };
76
+ }
77
+ return {
78
+ reconnect: true,
79
+ reason: `Mongo connection unhealthy (readyState=${s.readyState}) for ${Math.round(s.msSinceUnhealthy / 1000)}s past grace — forcing disconnect+connect to re-resolve the host IP`,
80
+ };
81
+ }
82
+
83
+ /** Injektált függőségek a futtatóhoz (best-effort; a futtató sose dob). */
84
+ export interface MongoReconnectGuardDeps {
85
+ /** A jelenlegi `mongoose.connection.readyState`. */
86
+ getReadyState: () => number;
87
+ /**
88
+ * Force-reconnect: `disconnect()` majd `connect(uri, opts)` — ÚJ MongoClient → ÚJ DNS-resolve.
89
+ * Dobhat (a connect elbukhat, ha a Mongo épp tényleg down) — a futtató elkapja.
90
+ */
91
+ reconnect: () => Promise<void>;
92
+ /** Log. */
93
+ log: (msg: string) => void;
94
+ /** Tick-intervallum (ms). Default 10s. */
95
+ intervalMs?: number;
96
+ /** Grace, amíg a driver saját reconnect-jét várjuk (ms). Default 30s. */
97
+ unhealthyGraceMs?: number;
98
+ /** Cooldown két force-reconnect közt (ms). Default 20s. */
99
+ reconnectCooldownMs?: number;
100
+ }
101
+
102
+ /**
103
+ * Elindítja a periodikus reconnect-guard-ot. A visszaadott függvény leállítja (teszt/teardown).
104
+ * `.unref()` — nem tartja életben az event-loop-ot.
105
+ */
106
+ export function startMongoReconnectGuard(deps: MongoReconnectGuardDeps): () => void {
107
+ const intervalMs: number = deps.intervalMs ?? 10 * second;
108
+ const unhealthyGraceMs: number = deps.unhealthyGraceMs ?? 30 * second;
109
+ const reconnectCooldownMs: number = deps.reconnectCooldownMs ?? 20 * second;
110
+
111
+ let unhealthySinceMs: number | null = null;
112
+ let lastReconnectAtMs: number | null = null;
113
+ let inFlight: boolean = false;
114
+ let ticking: boolean = false;
115
+
116
+ const tick = async (): Promise<void> => {
117
+ if (ticking) { return; }
118
+ ticking = true;
119
+ try {
120
+ const nowMs: number = Date.now();
121
+ const readyState: number = deps.getReadyState();
122
+
123
+ if (readyState === 1) {
124
+ if (unhealthySinceMs !== null) {
125
+ deps.log(`[mongo-reconnect-guard] Mongo connection healthy again (was unhealthy ${Math.round((nowMs - unhealthySinceMs) / 1000)}s)`);
126
+ }
127
+ unhealthySinceMs = null;
128
+ return;
129
+ }
130
+
131
+ // Nem-connected: indítsuk/folytassuk az unhealthy-órát.
132
+ if (unhealthySinceMs === null) { unhealthySinceMs = nowMs; }
133
+
134
+ const decision: MongoReconnectDecision = shouldForceMongoReconnect({
135
+ readyState: readyState,
136
+ msSinceUnhealthy: nowMs - unhealthySinceMs,
137
+ unhealthyGraceMs: unhealthyGraceMs,
138
+ reconnectInFlight: inFlight,
139
+ msSinceLastReconnect: lastReconnectAtMs === null ? Number.POSITIVE_INFINITY : nowMs - lastReconnectAtMs,
140
+ reconnectCooldownMs: reconnectCooldownMs,
141
+ });
142
+
143
+ if (!decision.reconnect) { return; }
144
+
145
+ deps.log(`[mongo-reconnect-guard] ${decision.reason}`);
146
+ inFlight = true;
147
+ lastReconnectAtMs = nowMs;
148
+ try {
149
+ await deps.reconnect();
150
+ deps.log('[mongo-reconnect-guard] force-reconnect dispatched ✓ (re-resolved host; awaiting connection open)');
151
+ } catch (e: unknown) {
152
+ // A connect elbukhat, ha a Mongo épp tényleg elérhetetlen — nem baj, a következő tick
153
+ // (cooldown után) újrapróbál; amint a Mongo új IP-n elérhető, a re-resolve becsatlakozik.
154
+ deps.log(`[mongo-reconnect-guard] force-reconnect attempt failed (will retry after cooldown): ${e instanceof Error ? e.message : String(e)}`);
155
+ } finally {
156
+ inFlight = false;
157
+ }
158
+ } catch (e: unknown) {
159
+ deps.log(`[mongo-reconnect-guard] tick error (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
160
+ } finally {
161
+ ticking = false;
162
+ }
163
+ };
164
+
165
+ const handle: NodeJS.Timeout = setInterval((): void => { void tick(); }, intervalMs);
166
+ if (typeof handle.unref === 'function') { handle.unref(); }
167
+ DyFM_Log.info(
168
+ `[mongo-reconnect-guard] started — check every ${Math.round(intervalMs / 1000)}s, `
169
+ + `force disconnect+reconnect after ${Math.round(unhealthyGraceMs / 1000)}s sustained-unhealthy (re-resolves host IP), cooldown ${Math.round(reconnectCooldownMs / 1000)}s`,
170
+ );
171
+ return (): void => { clearInterval(handle); };
172
+ }
@@ -122,6 +122,28 @@ export interface DyNTS_Global_Settings {
122
122
  */
123
123
  assistant_settings?: any;
124
124
 
125
+ /**
126
+ * Auth-service beállítások.
127
+ *
128
+ * Ha `optional: true`, a framework NEM követeli meg az egyedi Auth Service-t:
129
+ * a {@link DyNTS_GlobalService.getAuthService} hiányzó service esetén `undefined`-ot
130
+ * ad vissza a `"Unique Authentication Service missing!"` hiba DOBÁSA HELYETT, és
131
+ * a startup-warn ('Authentication Service missing!') is elmarad. Public / no-auth
132
+ * szervereknek (pl. nyílt RAG/MCP backend), ahol nincs saját auth-réteg.
133
+ *
134
+ * Default: `{ optional: false }` — az auth-service kötelező (visszafelé kompatibilis,
135
+ * a régi viselkedés: hiányzó service → dobott hiba).
136
+ */
137
+ auth_settings?: {
138
+ /**
139
+ * Ha `true`, az Auth Service opcionális: `getAuthService()` NEM dob hiányzó
140
+ * service esetén, hanem `undefined`-ot ad vissza (a hívók optional-chaining-gel
141
+ * kezelik), és a startup 'Authentication Service missing!' warn is elmarad.
142
+ * Default: `false`.
143
+ */
144
+ optional?: boolean;
145
+ };
146
+
125
147
  /**
126
148
  * this setting will set the doc chunking settings
127
149
  */
@@ -128,6 +128,23 @@ describe('| DyNTS_GlobalService', () => {
128
128
 
129
129
  expect(result).toBe(authService);
130
130
  });
131
+
132
+ it('| should NOT throw and return undefined when auth_settings.optional is true', () => {
133
+ // Suppress: public / no-auth szerver — a beállítás elnyomja a
134
+ // "Unique Authentication Service missing!" dobást. A flag-et finally-ben
135
+ // visszaállítjuk, hogy a randomizált futásban ne szivárogjon más tesztbe.
136
+ const prev = DyNTS_global_settings.auth_settings?.optional;
137
+ try {
138
+ DyNTS_global_settings.auth_settings = { optional: true };
139
+ let result: DyNTS_AuthService;
140
+ expect(() => {
141
+ result = DyNTS_GlobalService.getAuthService();
142
+ }).not.toThrow();
143
+ expect(result).toBeUndefined();
144
+ } finally {
145
+ DyNTS_global_settings.auth_settings = { optional: prev ?? false };
146
+ }
147
+ });
131
148
  });
132
149
 
133
150
  describe('| getDBServiceCollection', () => {
@@ -212,7 +212,10 @@ export class DyNTS_GlobalService extends DyNTS_SingletonService {
212
212
  private static async setAuthService(authService?: DyNTS_AuthService): Promise<void> {
213
213
  try {
214
214
  if (!authService) {
215
- DyFM_Log.warn(`Authentication Service missing!`);
215
+ // Opcionális auth-service esetén ez VÁRT (public / no-auth szerver) — nincs warn.
216
+ if (!DyNTS_global_settings.auth_settings?.optional) {
217
+ DyFM_Log.warn(`Authentication Service missing!`);
218
+ }
216
219
  } else {
217
220
  this.instance.authService = authService;
218
221
  }
@@ -300,16 +303,27 @@ export class DyNTS_GlobalService extends DyNTS_SingletonService {
300
303
  */
301
304
  static getAuthService(): DyNTS_AuthService {
302
305
  if (!this.instance?.authService) {
306
+ // Ha az app explicit opcionálisnak jelölte az auth-service-t
307
+ // (DyNTS_global_settings.auth_settings.optional), NEM dobunk: undefined-ot
308
+ // adunk vissza. A hívók (pl. a request-path issuer-kinyerés) optional-
309
+ // chaining-gel kezelik. Public / no-auth szervereknek — így a
310
+ // "Unique Authentication Service missing!" hiba suppress-elhető beállításból.
311
+ if (DyNTS_global_settings.auth_settings?.optional) {
312
+ return undefined;
313
+ }
314
+
303
315
  throw new Error(
304
- `\n Unique Authentication Service missing!` +
305
- `\n Please create a Unique Authentication Service extending DyNTS_AuthService, ` +
316
+ `\n Unique Authentication Service missing!` +
317
+ `\n Please create a Unique Authentication Service extending DyNTS_AuthService, ` +
306
318
  `\n and Setup with DyNTS_GlobalServiceC.setServices(...)` +
307
319
  '\n (If you set the globalErrorHandler, ' +
308
- 'please check if it is using the same node_modules as the app)',
320
+ 'please check if it is using the same node_modules as the app)' +
321
+ '\n (To run WITHOUT an auth service, set ' +
322
+ 'DyNTS_global_settings.auth_settings.optional = true)',
309
323
  );
310
324
  }
311
325
 
312
- return this.instance.authService;
326
+ return this.instance.authService;
313
327
  }
314
328
 
315
329
  /**
@@ -29,6 +29,7 @@ import {
29
29
  import { DyNTS_defaultFallbackCacheMaxAge } from '../../_collections/default-fallback-cache-max-age.const';
30
30
  import { DyNTS_defaultNotFoundPageHtml } from '../../_collections/default-not-found-page.const';
31
31
  import { DyNTS_global_settings } from '../../_collections/global-settings.const';
32
+ import { startMongoReconnectGuard } from '../../_collections/mongo-reconnect-guard.util';
32
33
  import { DyNTS_RouteSecurity } from '../../_enums/route-security.enum';
33
34
  import { DyNTS_App_Params } from '../../_models/control-models/app-params.control-model';
34
35
  import {
@@ -985,10 +986,30 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
985
986
  } catch (error) {
986
987
  throw new DyFM_Error({
987
988
  ...this._getDefaultErrorSettings('startDB', error),
988
-
989
+
989
990
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AS0-SDB0`,
990
991
  });
991
992
  }
993
+
994
+ // 2026-06-20 — Mongo reconnect-guard. A MongoDB-driver a hostnevet connect-kor EGYSZER
995
+ // resolválja + cache-eli az IP-t; ha a Mongo-konténert recreate-elik (új docker-network IP),
996
+ // a driver a HALOTT IP-n ragad → ECONNREFUSED + buffering-timeout, és a service magától SOHA
997
+ // nem áll vissza. A guard sustained-disconnect (readyState !== 1) esetén — a driver saját
998
+ // grace-e UTÁN — TELJES disconnect()+connect()-et csinál → ÚJ MongoClient → ÚJ DNS-resolve →
999
+ // új IP → reconnect. Best-effort, always-on, healthy connection-t (===1) SOHA nem bánt; csak
1000
+ // konténer-IP-frissítés, nem ír adatot. Lásd: mongo-reconnect-guard.util.ts.
1001
+ try {
1002
+ startMongoReconnectGuard({
1003
+ getReadyState: (): number => this.mongoose.connection.readyState,
1004
+ reconnect: async (): Promise<void> => {
1005
+ await this.mongoose.disconnect().catch((): void => undefined);
1006
+ await this.mongoose.connect(this._params.dbUri, this._params.dbOptions);
1007
+ },
1008
+ log: (msg: string): void => DyFM_Log.warn(msg),
1009
+ });
1010
+ } catch (guardErr) {
1011
+ DyFM_Log.warn(`[mongo-reconnect-guard] failed to start (non-fatal): ${guardErr instanceof Error ? guardErr.message : String(guardErr)}`);
1012
+ }
992
1013
  }
993
1014
 
994
1015
  /**