@futdevpro/nts-dynamo 1.15.75 → 1.15.80

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 (38) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1952 -1883
  2. package/.dynamo/logs/cicd-pipeline/status.json +38 -38
  3. package/build/_collections/global-settings.const.d.ts.map +1 -1
  4. package/build/_collections/global-settings.const.js +11 -0
  5. package/build/_collections/global-settings.const.js.map +1 -1
  6. package/build/_models/interfaces/global-settings.interface.d.ts +18 -0
  7. package/build/_models/interfaces/global-settings.interface.d.ts.map +1 -1
  8. package/build/_services/base/data.service.d.ts +8 -0
  9. package/build/_services/base/data.service.d.ts.map +1 -1
  10. package/build/_services/base/data.service.js +17 -0
  11. package/build/_services/base/data.service.js.map +1 -1
  12. package/build/_services/core/collection-growth-monitor.service.d.ts +77 -0
  13. package/build/_services/core/collection-growth-monitor.service.d.ts.map +1 -0
  14. package/build/_services/core/collection-growth-monitor.service.js +147 -0
  15. package/build/_services/core/collection-growth-monitor.service.js.map +1 -0
  16. package/build/_services/core/global.service.d.ts +7 -0
  17. package/build/_services/core/global.service.d.ts.map +1 -1
  18. package/build/_services/core/global.service.js +10 -0
  19. package/build/_services/core/global.service.js.map +1 -1
  20. package/build/_services/server/app.server.d.ts +45 -0
  21. package/build/_services/server/app.server.d.ts.map +1 -1
  22. package/build/_services/server/app.server.js +97 -173
  23. package/build/_services/server/app.server.js.map +1 -1
  24. package/build/index.d.ts +1 -0
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js +1 -0
  27. package/build/index.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/_collections/global-settings.const.ts +12 -0
  30. package/src/_models/interfaces/global-settings.interface.ts +19 -0
  31. package/src/_services/base/data.service.spec.ts +56 -1
  32. package/src/_services/base/data.service.ts +23 -2
  33. package/src/_services/core/collection-growth-monitor.service.spec.ts +67 -0
  34. package/src/_services/core/collection-growth-monitor.service.ts +211 -0
  35. package/src/_services/core/global.service.ts +14 -2
  36. package/src/_services/server/app.server-retention.spec.ts +106 -0
  37. package/src/_services/server/app.server.ts +137 -3
  38. package/src/index.ts +1 -0
@@ -20,8 +20,9 @@ import {
20
20
  DyFM_Error,
21
21
  DyFM_error_defaults,
22
22
  DyFM_Error_Settings,
23
- DyFM_ErrorLevel,
23
+ DyFM_ErrorLevel,
24
24
  DyFM_Log,
25
+ DyFM_resolveRetentionTtlSeconds,
25
26
  megabyte,
26
27
  second
27
28
  } from '@futdevpro/fsm-dynamo';
@@ -51,8 +52,10 @@ import {
51
52
  import {
52
53
  DyNTS_Cors_Settings
53
54
  } from '../../_models/interfaces/cors-settings.interface';
55
+ import { DyNTS_DBService } from '../base/db.service';
54
56
  import { DyNTS_SingletonService } from '../base/singleton.service';
55
57
  import { DyNTS_GlobalService } from '../core/global.service';
58
+ import { DyNTS_CollectionGrowthMonitor } from '../core/collection-growth-monitor.service';
56
59
  import { DyNTS_MemoryGuard } from '../core/memory-guard.service';
57
60
  import { DyNTS_RoutingModule } from '../route/routing-module.service';
58
61
  import { DyNTS_getStarRoute } from '../../_collections/star.controller';
@@ -230,8 +233,30 @@ import { DyNTS_getStarRoute } from '../../_collections/star.controller';
230
233
  *
231
234
  *
232
235
  * ```
233
- *
236
+ *
237
+ */
238
+
239
+ /** FR-258 / SR-2 — one entry from `collection.indexes()` (only the fields the TTL-installer reads). */
240
+ export interface DyNTS_TtlIndexInfo {
241
+ name?: string;
242
+ key?: Record<string, number>;
243
+ expireAfterSeconds?: number;
244
+ }
245
+
246
+ /**
247
+ * FR-258 / SR-2 — the minimal native-driver collection surface the TTL-installer needs. Declared
248
+ * structurally (not importing mongodb types) so it stays dependency-light and is trivially mockable
249
+ * in unit tests.
234
250
  */
251
+ export interface DyNTS_TtlIndexCollection {
252
+ indexes?: () => Promise<DyNTS_TtlIndexInfo[]>;
253
+ createIndex: (keys: Record<string, number>, options: { expireAfterSeconds: number }) => Promise<unknown>;
254
+ dropIndex?: (name: string) => Promise<unknown>;
255
+ }
256
+
257
+ /** FR-258 / SR-2 — outcome of {@link DyNTS_App.ensureRetentionTtlIndex}. */
258
+ export type DyNTS_TtlIndexAction = 'created' | 'updated' | 'noop';
259
+
235
260
  export abstract class DyNTS_App extends DyNTS_SingletonService {
236
261
 
237
262
  protected systemControls: DyNTS_AppSystemControls = new DyNTS_AppSystemControls();
@@ -555,6 +580,17 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
555
580
  DyFM_Log.warn('[DyNTS_MemoryGuard] auto-install skipped (non-fatal):', memoryGuardError);
556
581
  }
557
582
 
583
+ // FR-258 / SR-4 — proactive collection-growth monitor (companion to the MemoryGuard). Warns
584
+ // into the Errors-sink when a collection grows unbounded, BEFORE it can be loaded into heap and
585
+ // OOM. Default-on, observation-only, never throws.
586
+ try {
587
+ if (DyNTS_global_settings.collectionGrowthMonitor?.enabled) {
588
+ DyNTS_CollectionGrowthMonitor.getInstance().install();
589
+ }
590
+ } catch (cgmError: unknown) {
591
+ DyFM_Log.warn('[DyNTS_CollectionGrowthMonitor] auto-install skipped (non-fatal):', cgmError);
592
+ }
593
+
558
594
  if (!extended) {
559
595
  await this.ready();
560
596
 
@@ -916,6 +952,11 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
916
952
  DyFM_Log.success(`\nConnected to MongoDB (${this._params.dbUri})\n`);
917
953
 
918
954
  resolve();
955
+
956
+ // FR-258 / SR-2 — install declared-retention TTL indexes. Fire-and-forget +
957
+ // fully non-fatal: an index build on a large collection must NOT block startup,
958
+ // and any failure here is logged but never crashes the app.
959
+ void this.installRetentionTtlIndexes();
919
960
  })
920
961
  .on('error', (error): void => {
921
962
  if (!this.systemControls.mongoose.started) {
@@ -1013,7 +1054,100 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1013
1054
  }
1014
1055
 
1015
1056
  /**
1016
- *
1057
+ * FR-258 / SR-2 — install MongoDB TTL indexes for every data-model that declared `retention`.
1058
+ *
1059
+ * Runs ONCE right after the DB connection opens. For each registered DB service (including the
1060
+ * auto-created `_archived` siblings, which inherit the parent model's `retention`), it ensures a
1061
+ * TTL index on the `__created` Date field with the resolved `expireAfterSeconds` — so MongoDB
1062
+ * NATIVELY auto-deletes documents older than the configured age. This is the fleet-wide defense
1063
+ * against the unbounded-collection-growth → in-memory-load → GC-thrash/OOM class.
1064
+ *
1065
+ * Guarantees:
1066
+ * - **Non-fatal** — any error is logged but NEVER blocks startup (called fire-and-forget).
1067
+ * - **Idempotent** — an existing index with the same TTL is a no-op; a CHANGED retention (different
1068
+ * `expireAfterSeconds`, or an existing non-TTL `__created` index) is reconciled by drop+recreate
1069
+ * (the field index is restored immediately, so query coverage is preserved).
1070
+ */
1071
+ private async installRetentionTtlIndexes(): Promise<void> {
1072
+ try {
1073
+ const services: DyNTS_DBService<any>[] = DyNTS_GlobalService.getAllDBServices();
1074
+ let ensured: number = 0;
1075
+
1076
+ for (const service of services) {
1077
+ const ttlSeconds: number | undefined = DyFM_resolveRetentionTtlSeconds(service?.dataParams?.retention);
1078
+ if (!ttlSeconds) { continue; }
1079
+
1080
+ const dataName: string = service.dataParams.dataName;
1081
+ // The mongoose Model exposes the native driver collection (.indexes / .createIndex / .dropIndex).
1082
+ const collection: DyNTS_TtlIndexCollection | undefined =
1083
+ (service?.dataModel as unknown as { collection?: DyNTS_TtlIndexCollection })?.collection;
1084
+ if (!collection || typeof collection.createIndex !== 'function') { continue; }
1085
+
1086
+ try {
1087
+ const action: DyNTS_TtlIndexAction = await DyNTS_App.ensureRetentionTtlIndex(collection, ttlSeconds);
1088
+ if (action === 'created' || action === 'updated') {
1089
+ ensured++;
1090
+ if (this.logSetup) {
1091
+ DyFM_Log.success(
1092
+ `[FR-258 retention] TTL index ${action} on "${dataName}" — ` +
1093
+ `${Math.round(ttlSeconds / 86400)}d (${ttlSeconds}s)`,
1094
+ );
1095
+ }
1096
+ }
1097
+ } catch (idxErr: unknown) {
1098
+ DyFM_Log.warn(
1099
+ `[FR-258 retention] TTL index on "${dataName}" failed (non-fatal): ` +
1100
+ `${idxErr instanceof Error ? idxErr.message : String(idxErr)}`,
1101
+ );
1102
+ }
1103
+ }
1104
+
1105
+ if (ensured > 0) {
1106
+ DyFM_Log.success(`[FR-258 retention] ${ensured} TTL index(es) ensured (auto-retention active).`);
1107
+ }
1108
+ } catch (err: unknown) {
1109
+ DyFM_Log.warn(
1110
+ `[FR-258 retention] TTL-installer failed (non-fatal): ` +
1111
+ `${err instanceof Error ? err.message : String(err)}`,
1112
+ );
1113
+ }
1114
+ }
1115
+
1116
+ /**
1117
+ * FR-258 / SR-2 — ensure a single `{ __created: 1 }` TTL index with the given `expireAfterSeconds`.
1118
+ * Extracted as a pure-ish static so it is unit-testable with a mock collection. Idempotent:
1119
+ * - no existing `__created` index → create it → `'created'`
1120
+ * - existing with the SAME TTL → no-op → `'noop'`
1121
+ * - existing with a DIFFERENT TTL / non-TTL → drop + recreate → `'updated'`
1122
+ * Drop+recreate preserves query coverage (the `{__created:1}` index is restored immediately).
1123
+ */
1124
+ static async ensureRetentionTtlIndex(
1125
+ collection: DyNTS_TtlIndexCollection,
1126
+ ttlSeconds: number,
1127
+ ): Promise<DyNTS_TtlIndexAction> {
1128
+ const existingList: DyNTS_TtlIndexInfo[] = typeof collection.indexes === 'function'
1129
+ ? await collection.indexes().catch((): DyNTS_TtlIndexInfo[] => [])
1130
+ : [];
1131
+ const existing: DyNTS_TtlIndexInfo | undefined =
1132
+ existingList.find((ix): boolean => !!ix?.key && ix.key.__created === 1);
1133
+
1134
+ if (!existing) {
1135
+ await collection.createIndex({ __created: 1 }, { expireAfterSeconds: ttlSeconds });
1136
+ return 'created';
1137
+ }
1138
+ if (existing.expireAfterSeconds === ttlSeconds) {
1139
+ return 'noop';
1140
+ }
1141
+ // Retention changed, or an existing non-TTL `__created` index → reconcile by drop+recreate.
1142
+ if (existing.name && typeof collection.dropIndex === 'function') {
1143
+ await collection.dropIndex(existing.name).catch((): void => undefined);
1144
+ }
1145
+ await collection.createIndex({ __created: 1 }, { expireAfterSeconds: ttlSeconds });
1146
+ return 'updated';
1147
+ }
1148
+
1149
+ /**
1150
+ *
1017
1151
  */
1018
1152
  private async initExpresses(): Promise<void> {
1019
1153
  if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. initExpresses');
package/src/index.ts CHANGED
@@ -71,6 +71,7 @@ export * from './_services/core/api.service';
71
71
  export * from './_services/core/auth.service';
72
72
 
73
73
  export * from './_services/core/email.service';
74
+ export * from './_services/core/collection-growth-monitor.service';
74
75
  export * from './_services/core/global.service';
75
76
  export * from './_services/core/memory-guard.service';
76
77