@dudousxd/nestjs-durable-core 0.74.0 → 0.75.0

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,8 @@ 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;
3800
3804
  constructor(deps: WorkflowEngineDeps);
3801
3805
  /**
3802
3806
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4050,10 +4054,25 @@ declare class WorkflowEngine {
4050
4054
  private findParentRunId;
4051
4055
  /** Track an in-flight execution so {@link drain} can wait for it. */
4052
4056
  private track;
4057
+ /**
4058
+ * Stop picking up new work without waiting: recovery, timer, schedule and timeout sweeps (and
4059
+ * their per-run lock loops) start nothing once this is called, and {@link runOne} returns null. A
4060
+ * poller calls it on its own shutdown so an in-flight sweep stops locking runs before it is awaited.
4061
+ *
4062
+ * `spentMs` is wait time ALREADY spent on this shutdown (the poller awaiting its in-flight sweep).
4063
+ * It is deducted from the budget of a later {@link drain}, so that wait plus the drain stay within
4064
+ * one `shutdownTimeoutMs`. Only time actually spent waiting is charged: time that merely passes
4065
+ * between this call and `drain()` (other hooks, the HTTP server closing) never is. Calls add up.
4066
+ */
4067
+ beginDrain(spentMs?: number): void;
4053
4068
  /**
4054
4069
  * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
4055
4070
  * in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
4056
4071
  * deploy hands off cleanly instead of leaving runs to the lease timeout.
4072
+ *
4073
+ * `timeoutMs` is the total budget for waiting on in-flight runs; any wait already reported through
4074
+ * {@link beginDrain} (`spentMs`) is deducted, so a shutdown that already waited on the timer poll
4075
+ * for part of it waits only for the remainder. With nothing reported, the full `timeoutMs` applies.
4057
4076
  */
4058
4077
  drain(timeoutMs?: number): Promise<void>;
4059
4078
  /**
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,8 @@ 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;
3800
3804
  constructor(deps: WorkflowEngineDeps);
3801
3805
  /**
3802
3806
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4050,10 +4054,25 @@ declare class WorkflowEngine {
4050
4054
  private findParentRunId;
4051
4055
  /** Track an in-flight execution so {@link drain} can wait for it. */
4052
4056
  private track;
4057
+ /**
4058
+ * Stop picking up new work without waiting: recovery, timer, schedule and timeout sweeps (and
4059
+ * their per-run lock loops) start nothing once this is called, and {@link runOne} returns null. A
4060
+ * poller calls it on its own shutdown so an in-flight sweep stops locking runs before it is awaited.
4061
+ *
4062
+ * `spentMs` is wait time ALREADY spent on this shutdown (the poller awaiting its in-flight sweep).
4063
+ * It is deducted from the budget of a later {@link drain}, so that wait plus the drain stay within
4064
+ * one `shutdownTimeoutMs`. Only time actually spent waiting is charged: time that merely passes
4065
+ * between this call and `drain()` (other hooks, the HTTP server closing) never is. Calls add up.
4066
+ */
4067
+ beginDrain(spentMs?: number): void;
4053
4068
  /**
4054
4069
  * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
4055
4070
  * in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
4056
4071
  * deploy hands off cleanly instead of leaving runs to the lease timeout.
4072
+ *
4073
+ * `timeoutMs` is the total budget for waiting on in-flight runs; any wait already reported through
4074
+ * {@link beginDrain} (`spentMs`) is deducted, so a shutdown that already waited on the timer poll
4075
+ * for part of it waits only for the remainder. With nothing reported, the full `timeoutMs` applies.
4057
4076
  */
4058
4077
  drain(timeoutMs?: number): Promise<void>;
4059
4078
  /**
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,8 @@ 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;
3708
3712
  constructor(deps) {
3709
3713
  this.store = deps.store;
3710
3714
  this.pool = new TransportPool(deps.transports ?? (deps.transport ? [
@@ -3753,7 +3757,8 @@ var WorkflowEngine = class {
3753
3757
  });
3754
3758
  this.schedules = new ScheduleClient(this.store, {
3755
3759
  start: /* @__PURE__ */ __name((workflow, input, runId, opts) => this.start(workflow, input, runId, opts), "start"),
3756
- getRun: /* @__PURE__ */ __name((runId) => this.store.getRun(runId), "getRun")
3760
+ getRun: /* @__PURE__ */ __name((runId) => this.store.getRun(runId), "getRun"),
3761
+ isDraining: /* @__PURE__ */ __name(() => this.draining, "isDraining")
3757
3762
  }, {
3758
3763
  namespace: this.namespace,
3759
3764
  clock: this.clock
@@ -4378,15 +4383,34 @@ var WorkflowEngine = class {
4378
4383
  return p;
4379
4384
  }
4380
4385
  /**
4386
+ * Stop picking up new work without waiting: recovery, timer, schedule and timeout sweeps (and
4387
+ * their per-run lock loops) start nothing once this is called, and {@link runOne} returns null. A
4388
+ * poller calls it on its own shutdown so an in-flight sweep stops locking runs before it is awaited.
4389
+ *
4390
+ * `spentMs` is wait time ALREADY spent on this shutdown (the poller awaiting its in-flight sweep).
4391
+ * It is deducted from the budget of a later {@link drain}, so that wait plus the drain stay within
4392
+ * one `shutdownTimeoutMs`. Only time actually spent waiting is charged: time that merely passes
4393
+ * between this call and `drain()` (other hooks, the HTTP server closing) never is. Calls add up.
4394
+ */
4395
+ beginDrain(spentMs = 0) {
4396
+ this.draining = true;
4397
+ this.drainSpentMs += Math.max(0, spentMs);
4398
+ }
4399
+ /**
4381
4400
  * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
4382
4401
  * in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
4383
4402
  * deploy hands off cleanly instead of leaving runs to the lease timeout.
4403
+ *
4404
+ * `timeoutMs` is the total budget for waiting on in-flight runs; any wait already reported through
4405
+ * {@link beginDrain} (`spentMs`) is deducted, so a shutdown that already waited on the timer poll
4406
+ * for part of it waits only for the remainder. With nothing reported, the full `timeoutMs` applies.
4384
4407
  */
4385
4408
  async drain(timeoutMs = 1e4) {
4386
- this.draining = true;
4409
+ this.beginDrain();
4410
+ const budget = Math.max(0, timeoutMs - this.drainSpentMs);
4387
4411
  if (this.inflight.size === 0) return;
4388
4412
  const timer = new Promise((resolve) => {
4389
- const t = setTimeout(resolve, timeoutMs);
4413
+ const t = setTimeout(resolve, budget);
4390
4414
  t.unref?.();
4391
4415
  });
4392
4416
  await Promise.race([
@@ -4428,6 +4452,7 @@ var WorkflowEngine = class {
4428
4452
  */
4429
4453
  async sweepTimeouts(now = this.clock()) {
4430
4454
  for (const reg of new Set(this.latest.values())) {
4455
+ if (this.draining) return;
4431
4456
  if (reg.executionTimeoutMs == null) continue;
4432
4457
  const deadline = now - reg.executionTimeoutMs;
4433
4458
  const inflight = [
@@ -4443,6 +4468,7 @@ var WorkflowEngine = class {
4443
4468
  })
4444
4469
  ];
4445
4470
  for (const run of inflight) {
4471
+ if (this.draining) return;
4446
4472
  if (run.createdAt.getTime() > deadline) continue;
4447
4473
  const error2 = {
4448
4474
  message: "execution timeout",
@@ -4472,6 +4498,7 @@ var WorkflowEngine = class {
4472
4498
  if (this.draining) return [];
4473
4499
  const results = [];
4474
4500
  for (const listed of await this.store.listIncompleteRuns(this.namespace)) {
4501
+ if (this.draining) break;
4475
4502
  const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4476
4503
  if (!acquired) continue;
4477
4504
  const run = await this.store.getRun(listed.id);
@@ -4907,8 +4934,13 @@ var WorkflowEngine = class {
4907
4934
  if (this.draining) return [];
4908
4935
  const results = [];
4909
4936
  for (const run of runs) {
4937
+ if (this.draining) break;
4910
4938
  const acquired = await this.store.tryLockRun(run.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4911
4939
  if (!acquired) continue;
4940
+ if (this.draining) {
4941
+ await this.store.releaseRunLock(run.id);
4942
+ break;
4943
+ }
4912
4944
  try {
4913
4945
  const settled = onLocked ? await onLocked(run) : void 0;
4914
4946
  results.push(settled ?? await this.resumeImplicit(run.id));