@dudousxd/nestjs-durable 0.45.1 → 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.cjs +85 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +86 -15
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.d.cts
CHANGED
|
@@ -1065,12 +1065,12 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
1065
1065
|
/**
|
|
1066
1066
|
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
1067
1067
|
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
1068
|
-
* re-deriving `listRuns`' waiter
|
|
1069
|
-
* `listRuns`' waiting computation (the
|
|
1068
|
+
* re-deriving `listRuns`' waiter read or querying `durable_step_checkpoints` directly. Mirrors
|
|
1069
|
+
* `listRuns`' waiting computation (the requested runs' signal waiters + `resolveRunWaiting`), but ALSO
|
|
1070
1070
|
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
1071
1071
|
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
1072
1072
|
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
1073
|
-
* Two bulk
|
|
1073
|
+
* Two bulk reads total (never one query per requested id), same as `listRuns`.
|
|
1074
1074
|
*/
|
|
1075
1075
|
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
1076
1076
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
package/dist/index.d.ts
CHANGED
|
@@ -1065,12 +1065,12 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
1065
1065
|
/**
|
|
1066
1066
|
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
1067
1067
|
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
1068
|
-
* re-deriving `listRuns`' waiter
|
|
1069
|
-
* `listRuns`' waiting computation (the
|
|
1068
|
+
* re-deriving `listRuns`' waiter read or querying `durable_step_checkpoints` directly. Mirrors
|
|
1069
|
+
* `listRuns`' waiting computation (the requested runs' signal waiters + `resolveRunWaiting`), but ALSO
|
|
1070
1070
|
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
1071
1071
|
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
1072
1072
|
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
1073
|
-
* Two bulk
|
|
1073
|
+
* Two bulk reads total (never one query per requested id), same as `listRuns`.
|
|
1074
1074
|
*/
|
|
1075
1075
|
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
1076
1076
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1468,7 +1513,7 @@ var RunRequestResponder = class {
|
|
|
1468
1513
|
};
|
|
1469
1514
|
|
|
1470
1515
|
// src/store-run-gateway.ts
|
|
1471
|
-
import { STATE_STORE_CANONICAL as STATE_STORE_CANONICAL2, WorkflowEngine as WorkflowEngine2, indexWaitersByRun, mergeRunFacetRows, resolveRunWaiting } from "@dudousxd/nestjs-durable-core";
|
|
1516
|
+
import { STATE_STORE_CANONICAL as STATE_STORE_CANONICAL2, WorkflowEngine as WorkflowEngine2, indexWaitersByRun, listSignalWaitersOfRuns, mergeRunFacetRows, resolveRunWaiting } from "@dudousxd/nestjs-durable-core";
|
|
1472
1517
|
import { Inject as Inject5, Injectable as Injectable8 } from "@nestjs/common";
|
|
1473
1518
|
function _ts_decorate8(decorators, target, key, desc) {
|
|
1474
1519
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -1526,7 +1571,7 @@ var StoreRunGateway = class {
|
|
|
1526
1571
|
}
|
|
1527
1572
|
async listRuns(query) {
|
|
1528
1573
|
const runs = await this.store.listRuns(query);
|
|
1529
|
-
const waiterByRun = indexWaitersByRun(await this.store.
|
|
1574
|
+
const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, runs.filter((run) => run.status === "suspended").map((run) => run.id)));
|
|
1530
1575
|
return runs.map((run) => {
|
|
1531
1576
|
const waiting = resolveRunWaiting(run, waiterByRun);
|
|
1532
1577
|
return waiting ? {
|
|
@@ -1563,12 +1608,12 @@ var StoreRunGateway = class {
|
|
|
1563
1608
|
/**
|
|
1564
1609
|
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
1565
1610
|
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
1566
|
-
* re-deriving `listRuns`' waiter
|
|
1567
|
-
* `listRuns`' waiting computation (the
|
|
1611
|
+
* re-deriving `listRuns`' waiter read or querying `durable_step_checkpoints` directly. Mirrors
|
|
1612
|
+
* `listRuns`' waiting computation (the requested runs' signal waiters + `resolveRunWaiting`), but ALSO
|
|
1568
1613
|
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
1569
1614
|
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
1570
1615
|
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
1571
|
-
* Two bulk
|
|
1616
|
+
* Two bulk reads total (never one query per requested id), same as `listRuns`.
|
|
1572
1617
|
*/
|
|
1573
1618
|
async waitingFor(runIds) {
|
|
1574
1619
|
if (runIds.length === 0) return {};
|
|
@@ -1579,7 +1624,7 @@ var StoreRunGateway = class {
|
|
|
1579
1624
|
"suspended"
|
|
1580
1625
|
]
|
|
1581
1626
|
}),
|
|
1582
|
-
this.store
|
|
1627
|
+
listSignalWaitersOfRuns(this.store, runIds)
|
|
1583
1628
|
]);
|
|
1584
1629
|
const waiterByRun = indexWaitersByRun(waiters);
|
|
1585
1630
|
const result = {};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
1747
|
-
if (this.polling) return;
|
|
1811
|
+
poll() {
|
|
1812
|
+
if (this.polling) return this.current ?? Promise.resolve();
|
|
1748
1813
|
this.polling = true;
|
|
1749
|
-
|
|
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}. */
|