@futdevpro/nts-dynamo 1.15.80 → 1.15.81

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.
@@ -254,8 +254,17 @@ export interface DyNTS_TtlIndexCollection {
254
254
  dropIndex?: (name: string) => Promise<unknown>;
255
255
  }
256
256
 
257
- /** FR-258 / SR-2 — outcome of {@link DyNTS_App.ensureRetentionTtlIndex}. */
258
- export type DyNTS_TtlIndexAction = 'created' | 'updated' | 'noop';
257
+ /**
258
+ * FR-258 / SR-2 outcome of {@link DyNTS_App.ensureRetentionTtlIndex}.
259
+ * - `created`: no `__created` TTL index existed → one was created.
260
+ * - `noop`: an index with the SAME TTL already existed → nothing done.
261
+ * - `differs`: an index exists with a DIFFERENT TTL (or a non-TTL `__created` index) → left UNTOUCHED,
262
+ * only a warning is emitted. We deliberately DO NOT drop+recreate at boot: rebuilding a TTL index on a
263
+ * multi-GB production collection is a heavy server-side op that can starve concurrent boot queries (e.g.
264
+ * the health-probe) and trip a fail-safe deploy revert. A deliberate retention CHANGE must be applied via
265
+ * maintenance, not silently at every startup.
266
+ */
267
+ export type DyNTS_TtlIndexAction = 'created' | 'differs' | 'noop';
259
268
 
260
269
  export abstract class DyNTS_App extends DyNTS_SingletonService {
261
270
 
@@ -953,10 +962,16 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
953
962
 
954
963
  resolve();
955
964
 
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();
965
+ // FR-258 / SR-2 — install declared-retention TTL indexes. DEFERRED + fire-and-forget +
966
+ // fully non-fatal: a create-if-missing index op (and the `indexes()` probes) must NOT
967
+ // compete with boot-readiness. We delay it well past the deploy health-probe window so a
968
+ // cold boot is never slowed by it, then run it detached. `.unref()` keeps it from holding
969
+ // the event loop open. Any failure is logged but never crashes the app. (`'differs'` never
970
+ // rebuilds a live index — see ensureRetentionTtlIndex.)
971
+ const ttlInstallTimer: ReturnType<typeof setTimeout> = setTimeout((): void => {
972
+ void this.installRetentionTtlIndexes();
973
+ }, 30000);
974
+ if (typeof ttlInstallTimer.unref === 'function') { ttlInstallTimer.unref(); }
960
975
  })
961
976
  .on('error', (error): void => {
962
977
  if (!this.systemControls.mongoose.started) {
@@ -1085,14 +1100,21 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1085
1100
 
1086
1101
  try {
1087
1102
  const action: DyNTS_TtlIndexAction = await DyNTS_App.ensureRetentionTtlIndex(collection, ttlSeconds);
1088
- if (action === 'created' || action === 'updated') {
1103
+ if (action === 'created') {
1089
1104
  ensured++;
1090
1105
  if (this.logSetup) {
1091
1106
  DyFM_Log.success(
1092
- `[FR-258 retention] TTL index ${action} on "${dataName}" — ` +
1107
+ `[FR-258 retention] TTL index created on "${dataName}" — ` +
1093
1108
  `${Math.round(ttlSeconds / 86400)}d (${ttlSeconds}s)`,
1094
1109
  );
1095
1110
  }
1111
+ } else if (action === 'differs') {
1112
+ // Live index has a different TTL — left untouched at boot (see ensureRetentionTtlIndex doc).
1113
+ DyFM_Log.warn(
1114
+ `[FR-258 retention] "${dataName}" already has a {__created} index with a DIFFERENT TTL than the ` +
1115
+ `declared ${Math.round(ttlSeconds / 86400)}d (${ttlSeconds}s) — left as-is (no boot-time rebuild). ` +
1116
+ `Apply the change via maintenance if intended.`,
1117
+ );
1096
1118
  }
1097
1119
  } catch (idxErr: unknown) {
1098
1120
  DyFM_Log.warn(
@@ -1115,11 +1137,16 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1115
1137
 
1116
1138
  /**
1117
1139
  * 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).
1140
+ * Extracted as a pure-ish static so it is unit-testable with a mock collection. Idempotent + BOOT-SAFE:
1141
+ * - no existing `__created` index → create it → `'created'`
1142
+ * - existing with the SAME TTL → no-op → `'noop'`
1143
+ * - existing with a DIFFERENT TTL / non-TTL → leave it as-is → `'differs'` (warn only — NO drop+recreate)
1144
+ *
1145
+ * The `'differs'` case intentionally does NOT mutate the live index. Dropping + recreating a TTL index on a
1146
+ * large (multi-GB) production collection is a heavy server-side operation; doing it automatically inside the
1147
+ * startup path can starve the concurrent boot queries (including the deploy health-probe) and trigger a
1148
+ * fail-safe revert. Create-if-missing is the essential growth-prevention behaviour; a deliberate retention
1149
+ * CHANGE on an already-indexed collection is a maintenance action, not a silent every-boot rebuild.
1123
1150
  */
1124
1151
  static async ensureRetentionTtlIndex(
1125
1152
  collection: DyNTS_TtlIndexCollection,
@@ -1138,12 +1165,9 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1138
1165
  if (existing.expireAfterSeconds === ttlSeconds) {
1139
1166
  return 'noop';
1140
1167
  }
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';
1168
+ // Retention differs from the live index (or it is a non-TTL `__created` index). Leave it UNTOUCHED at
1169
+ // boot only report it. Avoids a heavy drop+recreate competing with boot-readiness on a large collection.
1170
+ return 'differs';
1147
1171
  }
1148
1172
 
1149
1173
  /**