@dudousxd/nestjs-durable 0.46.0 → 0.46.1

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/dist/index.js CHANGED
@@ -1173,6 +1173,31 @@ ProxyRunGateway = _ts_decorate6([
1173
1173
  // src/retention-poller.ts
1174
1174
  import { DURABLE_OPTIONS_CANONICAL as DURABLE_OPTIONS_CANONICAL4, STATE_STORE_CANONICAL, TERMINAL_RUN_STATUSES, parseDuration } from "@dudousxd/nestjs-durable-core";
1175
1175
  import { Inject as Inject4, Injectable as Injectable7 } from "@nestjs/common";
1176
+
1177
+ // src/settle-within.ts
1178
+ async function settleWithin(p, timeoutMs) {
1179
+ let timer;
1180
+ let timedOut = false;
1181
+ const timeout = new Promise((resolve) => {
1182
+ timer = setTimeout(() => {
1183
+ timedOut = true;
1184
+ resolve();
1185
+ }, timeoutMs);
1186
+ timer.unref?.();
1187
+ });
1188
+ try {
1189
+ await Promise.race([
1190
+ p.then(void 0, () => void 0),
1191
+ timeout
1192
+ ]);
1193
+ } finally {
1194
+ if (timer) clearTimeout(timer);
1195
+ }
1196
+ return timedOut;
1197
+ }
1198
+ __name(settleWithin, "settleWithin");
1199
+
1200
+ // src/retention-poller.ts
1176
1201
  function _ts_decorate7(decorators, target, key, desc) {
1177
1202
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1178
1203
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1243,6 +1268,9 @@ var RetentionPoller = class {
1243
1268
  options;
1244
1269
  timer;
1245
1270
  sweeping = false;
1271
+ stopping = false;
1272
+ /** The sweep in flight, so shutdown can wait for it before the store closes. */
1273
+ current;
1246
1274
  constructor(store, options) {
1247
1275
  this.store = store;
1248
1276
  this.options = options;
@@ -1270,8 +1298,16 @@ var RetentionPoller = class {
1270
1298
  this.timer.unref?.();
1271
1299
  }
1272
1300
  }
1273
- onModuleDestroy() {
1301
+ /** Stop sweeping and wait for the sweep in flight (bounded by `shutdownTimeoutMs`, default 10s),
1302
+ * so no prune is still pending when the host closes its connection pool. */
1303
+ async onModuleDestroy() {
1274
1304
  if (this.timer) clearInterval(this.timer);
1305
+ this.stopping = true;
1306
+ if (!this.current) return;
1307
+ const budget = this.options.shutdownTimeoutMs ?? 1e4;
1308
+ if (await settleWithin(this.current, budget)) {
1309
+ console.warn(`[nestjs-durable] retention sweep still running after ${budget}ms at shutdown; continuing`);
1310
+ }
1275
1311
  }
1276
1312
  /**
1277
1313
  * Prune one round.
@@ -1282,8 +1318,16 @@ var RetentionPoller = class {
1282
1318
  * Pruning history is the least urgent thing this package does — a `pruneTerminalRuns` that fails
1283
1319
  * on a locked table must cost this sweep and nothing more.
1284
1320
  */
1285
- async sweep() {
1286
- if (this.sweeping) return;
1321
+ sweep() {
1322
+ if (this.sweeping) return this.current ?? Promise.resolve();
1323
+ const run = this.sweepOnce().finally(() => {
1324
+ if (this.current === run) this.current = void 0;
1325
+ });
1326
+ this.current = run;
1327
+ return run;
1328
+ }
1329
+ async sweepOnce() {
1330
+ if (this.stopping) return;
1287
1331
  const retention = this.options.retention;
1288
1332
  const store = this.requireStore();
1289
1333
  const prune = store.pruneTerminalRuns;
@@ -1294,6 +1338,7 @@ var RetentionPoller = class {
1294
1338
  const now = Date.now();
1295
1339
  for (const policy of retention.policies) {
1296
1340
  for (let batch = 0; batch < MAX_BATCHES_PER_POLICY; batch++) {
1341
+ if (this.stopping) return;
1297
1342
  const deleted = await prune.call(store, policy, now, batchSize);
1298
1343
  if (deleted < batchSize) break;
1299
1344
  }
@@ -1705,6 +1750,8 @@ var TimerPoller = class {
1705
1750
  options;
1706
1751
  timer;
1707
1752
  polling = false;
1753
+ /** The sweep in flight, so shutdown can wait for it instead of racing the store's close. */
1754
+ current;
1708
1755
  unsubscribeEnqueued;
1709
1756
  constructor(engine, options) {
1710
1757
  this.engine = engine;
@@ -1724,9 +1771,27 @@ var TimerPoller = class {
1724
1771
  this.timer.unref?.();
1725
1772
  }
1726
1773
  }
1727
- onModuleDestroy() {
1774
+ /**
1775
+ * Stop polling and wait for the sweep in flight (bounded by `shutdownTimeoutMs`, default 10s).
1776
+ * Nest runs every `onModuleDestroy` before any `onApplicationShutdown`, so returning here only once
1777
+ * the sweep has settled means no store call is still pending when the registrar drains and the
1778
+ * host closes its connection pool. The engine is told to stop first so a sweep working through a
1779
+ * backlog stops locking runs instead of finishing the backlog. The time spent waiting is deducted
1780
+ * from the later `engine.drain()`, so poll wait plus drain stay within one `shutdownTimeoutMs`.
1781
+ */
1782
+ async onModuleDestroy() {
1728
1783
  if (this.timer) clearInterval(this.timer);
1729
1784
  this.unsubscribeEnqueued?.();
1785
+ if (!isDrivingOperator(this.options)) return;
1786
+ this.engine.beginDrain?.();
1787
+ if (!this.current) return;
1788
+ const budget = this.options.shutdownTimeoutMs ?? 1e4;
1789
+ const started = Date.now();
1790
+ const timedOut = await settleWithin(this.current, budget);
1791
+ this.engine.beginDrain?.(Date.now() - started);
1792
+ if (timedOut) {
1793
+ console.warn(`[nestjs-durable] timer poll still running after ${budget}ms at shutdown; continuing (it may report store errors if the host ends its pool now)`);
1794
+ }
1730
1795
  }
1731
1796
  /**
1732
1797
  * One sweep of the engine's recovery work.
@@ -1743,10 +1808,18 @@ var TimerPoller = class {
1743
1808
  * `recoverIncomplete` says nothing about whether durable timers should still fire, and skipping
1744
1809
  * the rest of the tick would silently widen one subsystem's outage into all of them.
1745
1810
  */
1746
- async poll() {
1747
- if (this.polling) return;
1811
+ poll() {
1812
+ if (this.polling) return this.current ?? Promise.resolve();
1748
1813
  this.polling = true;
1749
- try {
1814
+ const run = this.pollOnce().finally(() => {
1815
+ this.polling = false;
1816
+ if (this.current === run) this.current = void 0;
1817
+ });
1818
+ this.current = run;
1819
+ return run;
1820
+ }
1821
+ async pollOnce() {
1822
+ {
1750
1823
  await this.sweep("runPending", () => this.engine.runPending());
1751
1824
  await this.sweep("recoverIncomplete", () => this.engine.recoverIncomplete());
1752
1825
  await this.sweep("resumeDueTimers", () => this.engine.resumeDueTimers());
@@ -1758,8 +1831,6 @@ var TimerPoller = class {
1758
1831
  if (this.options.persistedSchedules) {
1759
1832
  await this.sweep("persistedSchedules", () => this.engine.schedules.tick());
1760
1833
  }
1761
- } finally {
1762
- this.polling = false;
1763
1834
  }
1764
1835
  }
1765
1836
  /** Run one sweep, absorbing and reporting its failure — see {@link poll}. */