@dudousxd/nestjs-durable 0.46.0 → 0.46.2

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.cjs CHANGED
@@ -1259,6 +1259,31 @@ ProxyRunGateway = _ts_decorate6([
1259
1259
  // src/retention-poller.ts
1260
1260
  var import_nestjs_durable_core8 = require("@dudousxd/nestjs-durable-core");
1261
1261
  var import_common7 = require("@nestjs/common");
1262
+
1263
+ // src/settle-within.ts
1264
+ async function settleWithin(p, timeoutMs) {
1265
+ let timer;
1266
+ let timedOut = false;
1267
+ const timeout = new Promise((resolve) => {
1268
+ timer = setTimeout(() => {
1269
+ timedOut = true;
1270
+ resolve();
1271
+ }, timeoutMs);
1272
+ timer.unref?.();
1273
+ });
1274
+ try {
1275
+ await Promise.race([
1276
+ p.then(void 0, () => void 0),
1277
+ timeout
1278
+ ]);
1279
+ } finally {
1280
+ if (timer) clearTimeout(timer);
1281
+ }
1282
+ return timedOut;
1283
+ }
1284
+ __name(settleWithin, "settleWithin");
1285
+
1286
+ // src/retention-poller.ts
1262
1287
  function _ts_decorate7(decorators, target, key, desc) {
1263
1288
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1264
1289
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1329,6 +1354,9 @@ var RetentionPoller = class {
1329
1354
  options;
1330
1355
  timer;
1331
1356
  sweeping = false;
1357
+ stopping = false;
1358
+ /** The sweep in flight, so shutdown can wait for it before the store closes. */
1359
+ current;
1332
1360
  constructor(store, options) {
1333
1361
  this.store = store;
1334
1362
  this.options = options;
@@ -1356,8 +1384,18 @@ var RetentionPoller = class {
1356
1384
  this.timer.unref?.();
1357
1385
  }
1358
1386
  }
1359
- onModuleDestroy() {
1387
+ /** Stop sweeping and wait for the sweep in flight (bounded by `shutdownTimeoutMs`, default 10s),
1388
+ * so no prune is still pending when the host closes its connection pool. This wait runs concurrently
1389
+ * with the timer poller's and is NOT deducted from the engine's drain budget (the retention poller
1390
+ * holds no engine), so a slow prune can add up to one more `shutdownTimeoutMs` before the drain. */
1391
+ async onModuleDestroy() {
1360
1392
  if (this.timer) clearInterval(this.timer);
1393
+ this.stopping = true;
1394
+ if (!this.current) return;
1395
+ const budget = this.options.shutdownTimeoutMs ?? 1e4;
1396
+ if (await settleWithin(this.current, budget)) {
1397
+ console.warn(`[nestjs-durable] retention sweep still running after ${budget}ms at shutdown; continuing`);
1398
+ }
1361
1399
  }
1362
1400
  /**
1363
1401
  * Prune one round.
@@ -1368,8 +1406,16 @@ var RetentionPoller = class {
1368
1406
  * Pruning history is the least urgent thing this package does — a `pruneTerminalRuns` that fails
1369
1407
  * on a locked table must cost this sweep and nothing more.
1370
1408
  */
1371
- async sweep() {
1372
- if (this.sweeping) return;
1409
+ sweep() {
1410
+ if (this.sweeping) return this.current ?? Promise.resolve();
1411
+ const run = this.sweepOnce().finally(() => {
1412
+ if (this.current === run) this.current = void 0;
1413
+ });
1414
+ this.current = run;
1415
+ return run;
1416
+ }
1417
+ async sweepOnce() {
1418
+ if (this.stopping) return;
1373
1419
  const retention = this.options.retention;
1374
1420
  const store = this.requireStore();
1375
1421
  const prune = store.pruneTerminalRuns;
@@ -1380,6 +1426,7 @@ var RetentionPoller = class {
1380
1426
  const now = Date.now();
1381
1427
  for (const policy of retention.policies) {
1382
1428
  for (let batch = 0; batch < MAX_BATCHES_PER_POLICY; batch++) {
1429
+ if (this.stopping) return;
1383
1430
  const deleted = await prune.call(store, policy, now, batchSize);
1384
1431
  if (deleted < batchSize) break;
1385
1432
  }
@@ -1791,6 +1838,8 @@ var TimerPoller = class {
1791
1838
  options;
1792
1839
  timer;
1793
1840
  polling = false;
1841
+ /** The sweep in flight, so shutdown can wait for it instead of racing the store's close. */
1842
+ current;
1794
1843
  unsubscribeEnqueued;
1795
1844
  constructor(engine, options) {
1796
1845
  this.engine = engine;
@@ -1810,9 +1859,27 @@ var TimerPoller = class {
1810
1859
  this.timer.unref?.();
1811
1860
  }
1812
1861
  }
1813
- onModuleDestroy() {
1862
+ /**
1863
+ * Stop polling and wait for the sweep in flight (bounded by `shutdownTimeoutMs`, default 10s).
1864
+ * Nest runs every `onModuleDestroy` before any `onApplicationShutdown`, so returning here only once
1865
+ * the sweep has settled means no store call is still pending when the registrar drains and the
1866
+ * host closes its connection pool. The engine is told to stop first so a sweep working through a
1867
+ * backlog stops locking runs instead of finishing the backlog. The time spent waiting is deducted
1868
+ * from the later `engine.drain()`, so poll wait plus drain stay within one `shutdownTimeoutMs`.
1869
+ */
1870
+ async onModuleDestroy() {
1814
1871
  if (this.timer) clearInterval(this.timer);
1815
1872
  this.unsubscribeEnqueued?.();
1873
+ if (!isDrivingOperator(this.options)) return;
1874
+ this.engine.beginDrain?.();
1875
+ if (!this.current) return;
1876
+ const budget = this.options.shutdownTimeoutMs ?? 1e4;
1877
+ const started = Date.now();
1878
+ const timedOut = await settleWithin(this.current, budget);
1879
+ this.engine.beginDrain?.(Date.now() - started);
1880
+ if (timedOut) {
1881
+ 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)`);
1882
+ }
1816
1883
  }
1817
1884
  /**
1818
1885
  * One sweep of the engine's recovery work.
@@ -1829,10 +1896,18 @@ var TimerPoller = class {
1829
1896
  * `recoverIncomplete` says nothing about whether durable timers should still fire, and skipping
1830
1897
  * the rest of the tick would silently widen one subsystem's outage into all of them.
1831
1898
  */
1832
- async poll() {
1833
- if (this.polling) return;
1899
+ poll() {
1900
+ if (this.polling) return this.current ?? Promise.resolve();
1834
1901
  this.polling = true;
1835
- try {
1902
+ const run = this.pollOnce().finally(() => {
1903
+ this.polling = false;
1904
+ if (this.current === run) this.current = void 0;
1905
+ });
1906
+ this.current = run;
1907
+ return run;
1908
+ }
1909
+ async pollOnce() {
1910
+ {
1836
1911
  await this.sweep("runPending", () => this.engine.runPending());
1837
1912
  await this.sweep("recoverIncomplete", () => this.engine.recoverIncomplete());
1838
1913
  await this.sweep("resumeDueTimers", () => this.engine.resumeDueTimers());
@@ -1844,8 +1919,6 @@ var TimerPoller = class {
1844
1919
  if (this.options.persistedSchedules) {
1845
1920
  await this.sweep("persistedSchedules", () => this.engine.schedules.tick());
1846
1921
  }
1847
- } finally {
1848
- this.polling = false;
1849
1922
  }
1850
1923
  }
1851
1924
  /** Run one sweep, absorbing and reporting its failure — see {@link poll}. */