@dudousxd/nestjs-durable-core 0.74.0 → 0.75.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.d.cts CHANGED
@@ -3385,6 +3385,8 @@ declare class ScheduleNotFoundError extends Error {
3385
3385
  interface ScheduleHost {
3386
3386
  start(workflow: string, input: unknown, runId: string, opts?: StartOptions): Promise<RunResult>;
3387
3387
  getRun(runId: string): Promise<WorkflowRun | null>;
3388
+ /** True once the engine has begun shutting down; `tick` fires nothing more after that. */
3389
+ isDraining?(): boolean;
3388
3390
  }
3389
3391
  /** Engine-level knobs for the client. */
3390
3392
  interface ScheduleClientOptions {
@@ -3797,6 +3799,10 @@ declare class WorkflowEngine {
3797
3799
  /** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
3798
3800
  private readonly inflight;
3799
3801
  private draining;
3802
+ /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
3803
+ private drainSpentMs;
3804
+ /** Re-arms the deadline of every `drain()` currently waiting, when more wait is reported. */
3805
+ private readonly drainRearms;
3800
3806
  constructor(deps: WorkflowEngineDeps);
3801
3807
  /**
3802
3808
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4050,10 +4056,25 @@ declare class WorkflowEngine {
4050
4056
  private findParentRunId;
4051
4057
  /** Track an in-flight execution so {@link drain} can wait for it. */
4052
4058
  private track;
4059
+ /**
4060
+ * Stop picking up new work without waiting: recovery, timer, schedule and timeout sweeps (and
4061
+ * their per-run lock loops) start nothing once this is called, and {@link runOne} returns null. A
4062
+ * poller calls it on its own shutdown so an in-flight sweep stops locking runs before it is awaited.
4063
+ *
4064
+ * `spentMs` is wait time ALREADY spent on this shutdown (the poller awaiting its in-flight sweep).
4065
+ * It is deducted from the budget of a later {@link drain}, so that wait plus the drain stay within
4066
+ * one `shutdownTimeoutMs`. Only time actually spent waiting is charged: time that merely passes
4067
+ * between this call and `drain()` (other hooks, the HTTP server closing) never is. Calls add up.
4068
+ */
4069
+ beginDrain(spentMs?: number): void;
4053
4070
  /**
4054
4071
  * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
4055
4072
  * in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
4056
4073
  * deploy hands off cleanly instead of leaving runs to the lease timeout.
4074
+ *
4075
+ * `timeoutMs` is the total budget for waiting on in-flight runs; any wait already reported through
4076
+ * {@link beginDrain} (`spentMs`) is deducted, so a shutdown that already waited on the timer poll
4077
+ * for part of it waits only for the remainder. With nothing reported, the full `timeoutMs` applies.
4057
4078
  */
4058
4079
  drain(timeoutMs?: number): Promise<void>;
4059
4080
  /**
package/dist/index.d.ts CHANGED
@@ -3385,6 +3385,8 @@ declare class ScheduleNotFoundError extends Error {
3385
3385
  interface ScheduleHost {
3386
3386
  start(workflow: string, input: unknown, runId: string, opts?: StartOptions): Promise<RunResult>;
3387
3387
  getRun(runId: string): Promise<WorkflowRun | null>;
3388
+ /** True once the engine has begun shutting down; `tick` fires nothing more after that. */
3389
+ isDraining?(): boolean;
3388
3390
  }
3389
3391
  /** Engine-level knobs for the client. */
3390
3392
  interface ScheduleClientOptions {
@@ -3797,6 +3799,10 @@ declare class WorkflowEngine {
3797
3799
  /** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
3798
3800
  private readonly inflight;
3799
3801
  private draining;
3802
+ /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
3803
+ private drainSpentMs;
3804
+ /** Re-arms the deadline of every `drain()` currently waiting, when more wait is reported. */
3805
+ private readonly drainRearms;
3800
3806
  constructor(deps: WorkflowEngineDeps);
3801
3807
  /**
3802
3808
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4050,10 +4056,25 @@ declare class WorkflowEngine {
4050
4056
  private findParentRunId;
4051
4057
  /** Track an in-flight execution so {@link drain} can wait for it. */
4052
4058
  private track;
4059
+ /**
4060
+ * Stop picking up new work without waiting: recovery, timer, schedule and timeout sweeps (and
4061
+ * their per-run lock loops) start nothing once this is called, and {@link runOne} returns null. A
4062
+ * poller calls it on its own shutdown so an in-flight sweep stops locking runs before it is awaited.
4063
+ *
4064
+ * `spentMs` is wait time ALREADY spent on this shutdown (the poller awaiting its in-flight sweep).
4065
+ * It is deducted from the budget of a later {@link drain}, so that wait plus the drain stay within
4066
+ * one `shutdownTimeoutMs`. Only time actually spent waiting is charged: time that merely passes
4067
+ * between this call and `drain()` (other hooks, the HTTP server closing) never is. Calls add up.
4068
+ */
4069
+ beginDrain(spentMs?: number): void;
4053
4070
  /**
4054
4071
  * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
4055
4072
  * in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
4056
4073
  * deploy hands off cleanly instead of leaving runs to the lease timeout.
4074
+ *
4075
+ * `timeoutMs` is the total budget for waiting on in-flight runs; any wait already reported through
4076
+ * {@link beginDrain} (`spentMs`) is deducted, so a shutdown that already waited on the timer poll
4077
+ * for part of it waits only for the remainder. With nothing reported, the full `timeoutMs` applies.
4057
4078
  */
4058
4079
  drain(timeoutMs?: number): Promise<void>;
4059
4080
  /**
package/dist/index.js CHANGED
@@ -2187,12 +2187,14 @@ var ScheduleClient = class {
2187
2187
  const store = this.requireStore();
2188
2188
  const started = [];
2189
2189
  for (let round = 0; round < 50; round++) {
2190
+ if (this.host.isDraining?.()) break;
2190
2191
  const due = await store.listSchedules({
2191
2192
  dueBy: nowMs,
2192
2193
  namespace: this.options.namespace,
2193
2194
  limit: TICK_BATCH
2194
2195
  });
2195
2196
  for (const record of due) {
2197
+ if (this.host.isDraining?.()) return started;
2196
2198
  const runId = await this.fire(record, nowMs);
2197
2199
  if (runId) started.push(runId);
2198
2200
  }
@@ -3705,6 +3707,10 @@ var WorkflowEngine = class {
3705
3707
  /** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
3706
3708
  inflight = /* @__PURE__ */ new Set();
3707
3709
  draining = false;
3710
+ /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
3711
+ drainSpentMs = 0;
3712
+ /** Re-arms the deadline of every `drain()` currently waiting, when more wait is reported. */
3713
+ drainRearms = /* @__PURE__ */ new Set();
3708
3714
  constructor(deps) {
3709
3715
  this.store = deps.store;
3710
3716
  this.pool = new TransportPool(deps.transports ?? (deps.transport ? [
@@ -3753,7 +3759,8 @@ var WorkflowEngine = class {
3753
3759
  });
3754
3760
  this.schedules = new ScheduleClient(this.store, {
3755
3761
  start: /* @__PURE__ */ __name((workflow, input, runId, opts) => this.start(workflow, input, runId, opts), "start"),
3756
- getRun: /* @__PURE__ */ __name((runId) => this.store.getRun(runId), "getRun")
3762
+ getRun: /* @__PURE__ */ __name((runId) => this.store.getRun(runId), "getRun"),
3763
+ isDraining: /* @__PURE__ */ __name(() => this.draining, "isDraining")
3757
3764
  }, {
3758
3765
  namespace: this.namespace,
3759
3766
  clock: this.clock
@@ -4378,23 +4385,59 @@ var WorkflowEngine = class {
4378
4385
  return p;
4379
4386
  }
4380
4387
  /**
4388
+ * Stop picking up new work without waiting: recovery, timer, schedule and timeout sweeps (and
4389
+ * their per-run lock loops) start nothing once this is called, and {@link runOne} returns null. A
4390
+ * poller calls it on its own shutdown so an in-flight sweep stops locking runs before it is awaited.
4391
+ *
4392
+ * `spentMs` is wait time ALREADY spent on this shutdown (the poller awaiting its in-flight sweep).
4393
+ * It is deducted from the budget of a later {@link drain}, so that wait plus the drain stay within
4394
+ * one `shutdownTimeoutMs`. Only time actually spent waiting is charged: time that merely passes
4395
+ * between this call and `drain()` (other hooks, the HTTP server closing) never is. Calls add up.
4396
+ */
4397
+ beginDrain(spentMs = 0) {
4398
+ this.draining = true;
4399
+ if (Number.isFinite(spentMs) && spentMs > 0) {
4400
+ this.drainSpentMs += spentMs;
4401
+ for (const rearm of this.drainRearms) rearm();
4402
+ }
4403
+ }
4404
+ /**
4381
4405
  * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
4382
4406
  * in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
4383
4407
  * deploy hands off cleanly instead of leaving runs to the lease timeout.
4408
+ *
4409
+ * `timeoutMs` is the total budget for waiting on in-flight runs; any wait already reported through
4410
+ * {@link beginDrain} (`spentMs`) is deducted, so a shutdown that already waited on the timer poll
4411
+ * for part of it waits only for the remainder. With nothing reported, the full `timeoutMs` applies.
4384
4412
  */
4385
4413
  async drain(timeoutMs = 1e4) {
4386
- this.draining = true;
4414
+ this.beginDrain();
4387
4415
  if (this.inflight.size === 0) return;
4416
+ const start = Date.now();
4417
+ let rearm = /* @__PURE__ */ __name(() => {
4418
+ }, "rearm");
4419
+ let timeout;
4388
4420
  const timer = new Promise((resolve) => {
4389
- const t = setTimeout(resolve, timeoutMs);
4390
- t.unref?.();
4421
+ rearm = /* @__PURE__ */ __name(() => {
4422
+ clearTimeout(timeout);
4423
+ const remaining = Math.max(0, start + timeoutMs - this.drainSpentMs - Date.now());
4424
+ timeout = setTimeout(resolve, remaining);
4425
+ timeout.unref?.();
4426
+ }, "rearm");
4427
+ rearm();
4391
4428
  });
4392
- await Promise.race([
4393
- Promise.allSettled([
4394
- ...this.inflight
4395
- ]),
4396
- timer
4397
- ]);
4429
+ this.drainRearms.add(rearm);
4430
+ try {
4431
+ await Promise.race([
4432
+ Promise.allSettled([
4433
+ ...this.inflight
4434
+ ]),
4435
+ timer
4436
+ ]);
4437
+ } finally {
4438
+ this.drainRearms.delete(rearm);
4439
+ clearTimeout(timeout);
4440
+ }
4398
4441
  }
4399
4442
  /**
4400
4443
  * Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
@@ -4428,6 +4471,7 @@ var WorkflowEngine = class {
4428
4471
  */
4429
4472
  async sweepTimeouts(now = this.clock()) {
4430
4473
  for (const reg of new Set(this.latest.values())) {
4474
+ if (this.draining) return;
4431
4475
  if (reg.executionTimeoutMs == null) continue;
4432
4476
  const deadline = now - reg.executionTimeoutMs;
4433
4477
  const inflight = [
@@ -4443,6 +4487,7 @@ var WorkflowEngine = class {
4443
4487
  })
4444
4488
  ];
4445
4489
  for (const run of inflight) {
4490
+ if (this.draining) return;
4446
4491
  if (run.createdAt.getTime() > deadline) continue;
4447
4492
  const error2 = {
4448
4493
  message: "execution timeout",
@@ -4472,6 +4517,7 @@ var WorkflowEngine = class {
4472
4517
  if (this.draining) return [];
4473
4518
  const results = [];
4474
4519
  for (const listed of await this.store.listIncompleteRuns(this.namespace)) {
4520
+ if (this.draining) break;
4475
4521
  const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4476
4522
  if (!acquired) continue;
4477
4523
  const run = await this.store.getRun(listed.id);
@@ -4907,8 +4953,13 @@ var WorkflowEngine = class {
4907
4953
  if (this.draining) return [];
4908
4954
  const results = [];
4909
4955
  for (const run of runs) {
4956
+ if (this.draining) break;
4910
4957
  const acquired = await this.store.tryLockRun(run.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4911
4958
  if (!acquired) continue;
4959
+ if (this.draining) {
4960
+ await this.store.releaseRunLock(run.id);
4961
+ break;
4962
+ }
4912
4963
  try {
4913
4964
  const settled = onLocked ? await onLocked(run) : void 0;
4914
4965
  results.push(settled ?? await this.resumeImplicit(run.id));