@dudousxd/nestjs-durable-core 0.85.0 → 0.85.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.d.cts CHANGED
@@ -7038,6 +7038,8 @@ interface ScheduledWorkflow<W extends WorkflowRef = WorkflowRef> {
7038
7038
  /**
7039
7039
  * Randomly delay the dispatch by up to this many ms BEFORE firing, to spread load when many
7040
7040
  * instances tick on the same boundary (avoids a thundering herd). Opt-in; absent = fire immediately.
7041
+ * The window is deferred to a later tick, never waited for inside one, so a jittered schedule does
7042
+ * not hold up any other schedule or the rest of the timer poll.
7041
7043
  *
7042
7044
  * This is dispatch-path jitter only — it does NOT affect the run id (still the time bucket), so
7043
7045
  * idempotency is unchanged: two instances jittering the same window still start it exactly once.
@@ -7066,9 +7068,13 @@ interface ScheduledWorkflow<W extends WorkflowRef = WorkflowRef> {
7066
7068
  namespace?: string;
7067
7069
  }
7068
7070
  /** Injectable dispatch-path effects (kept out of the deterministic run-id logic). Defaults to real
7069
- * `Math.random` + `setTimeout`; overridden in tests so jitter is deterministic and instant. */
7071
+ * `Math.random`; overridden in tests so jitter is deterministic. */
7070
7072
  interface RunSchedulesOptions {
7071
7073
  random?: () => number;
7074
+ /**
7075
+ * @deprecated No longer called: jitter defers a window to a later tick instead of sleeping inside
7076
+ * this one. Kept so existing callers still type-check.
7077
+ */
7072
7078
  sleep?: (ms: number) => Promise<void>;
7073
7079
  }
7074
7080
  /** Deterministic run id for a fixed-interval schedule's current time window. */
@@ -7096,10 +7102,14 @@ declare function scheduleRunIdAt(s: ScheduledWorkflow, nowMs: number): string;
7096
7102
  * Start each schedule's current-window run. The run id is the time bucket (a fixed-interval index,
7097
7103
  * or the cron fire time) and `engine.start` is idempotent, so firing this on an interval — or
7098
7104
  * racing two instances on the same tick — starts **each window exactly once**. Wire it to a
7099
- * `setInterval`, the durable timer poller, or `@nestjs/schedule`. Returns the run ids this call
7105
+ * `setInterval`, the durable timer poller, or `@nestjs/schedule`.
7106
+ *
7107
+ * Pass the SAME `engine` object on every call: which windows already fired, and the jittered fire
7108
+ * time of each deferred window, are remembered per engine object. A fresh wrapper per call would
7109
+ * redraw a deferred window's fire time on every tick, so it might never fire. Returns the run ids this call
7100
7110
  * started: a window that already has a run is not started or returned again, and once this process
7101
- * has seen a window's run exist, later calls in that window skip it without a store read or a
7102
- * jitter sleep.
7111
+ * has seen a window's run exist, later calls in that window skip it without a store read. A
7112
+ * jittered window is deferred to a later call, never slept on.
7103
7113
  */
7104
7114
  declare function runSchedules(engine: Pick<WorkflowEngine, 'start' | 'getRun'> & Partial<Pick<WorkflowEngine, 'isDraining'>>, schedules: readonly ScheduledWorkflow[], nowMs: number, opts?: RunSchedulesOptions): Promise<string[]>;
7105
7115
 
package/dist/index.d.ts CHANGED
@@ -7038,6 +7038,8 @@ interface ScheduledWorkflow<W extends WorkflowRef = WorkflowRef> {
7038
7038
  /**
7039
7039
  * Randomly delay the dispatch by up to this many ms BEFORE firing, to spread load when many
7040
7040
  * instances tick on the same boundary (avoids a thundering herd). Opt-in; absent = fire immediately.
7041
+ * The window is deferred to a later tick, never waited for inside one, so a jittered schedule does
7042
+ * not hold up any other schedule or the rest of the timer poll.
7041
7043
  *
7042
7044
  * This is dispatch-path jitter only — it does NOT affect the run id (still the time bucket), so
7043
7045
  * idempotency is unchanged: two instances jittering the same window still start it exactly once.
@@ -7066,9 +7068,13 @@ interface ScheduledWorkflow<W extends WorkflowRef = WorkflowRef> {
7066
7068
  namespace?: string;
7067
7069
  }
7068
7070
  /** Injectable dispatch-path effects (kept out of the deterministic run-id logic). Defaults to real
7069
- * `Math.random` + `setTimeout`; overridden in tests so jitter is deterministic and instant. */
7071
+ * `Math.random`; overridden in tests so jitter is deterministic. */
7070
7072
  interface RunSchedulesOptions {
7071
7073
  random?: () => number;
7074
+ /**
7075
+ * @deprecated No longer called: jitter defers a window to a later tick instead of sleeping inside
7076
+ * this one. Kept so existing callers still type-check.
7077
+ */
7072
7078
  sleep?: (ms: number) => Promise<void>;
7073
7079
  }
7074
7080
  /** Deterministic run id for a fixed-interval schedule's current time window. */
@@ -7096,10 +7102,14 @@ declare function scheduleRunIdAt(s: ScheduledWorkflow, nowMs: number): string;
7096
7102
  * Start each schedule's current-window run. The run id is the time bucket (a fixed-interval index,
7097
7103
  * or the cron fire time) and `engine.start` is idempotent, so firing this on an interval — or
7098
7104
  * racing two instances on the same tick — starts **each window exactly once**. Wire it to a
7099
- * `setInterval`, the durable timer poller, or `@nestjs/schedule`. Returns the run ids this call
7105
+ * `setInterval`, the durable timer poller, or `@nestjs/schedule`.
7106
+ *
7107
+ * Pass the SAME `engine` object on every call: which windows already fired, and the jittered fire
7108
+ * time of each deferred window, are remembered per engine object. A fresh wrapper per call would
7109
+ * redraw a deferred window's fire time on every tick, so it might never fire. Returns the run ids this call
7100
7110
  * started: a window that already has a run is not started or returned again, and once this process
7101
- * has seen a window's run exist, later calls in that window skip it without a store read or a
7102
- * jitter sleep.
7111
+ * has seen a window's run exist, later calls in that window skip it without a store read. A
7112
+ * jittered window is deferred to a later call, never slept on.
7103
7113
  */
7104
7114
  declare function runSchedules(engine: Pick<WorkflowEngine, 'start' | 'getRun'> & Partial<Pick<WorkflowEngine, 'isDraining'>>, schedules: readonly ScheduledWorkflow[], nowMs: number, opts?: RunSchedulesOptions): Promise<string[]>;
7105
7115
 
package/dist/index.js CHANGED
@@ -2417,7 +2417,31 @@ __name(resolveCronParse, "resolveCronParse");
2417
2417
 
2418
2418
  // src/scheduler.ts
2419
2419
  var nodeRequire = createRequire(import.meta.url);
2420
- var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
2420
+ var jitterFireAt = /* @__PURE__ */ new WeakMap();
2421
+ function deferredWindows(engine, key) {
2422
+ return [
2423
+ ...jitterFireAt.get(engine)?.get(key)?.keys() ?? []
2424
+ ];
2425
+ }
2426
+ __name(deferredWindows, "deferredWindows");
2427
+ function jitterDue(engine, s, candidates, nowMs, random) {
2428
+ let byKey = jitterFireAt.get(engine);
2429
+ if (byKey === void 0) {
2430
+ byKey = /* @__PURE__ */ new Map();
2431
+ jitterFireAt.set(engine, byKey);
2432
+ }
2433
+ const previous = byKey.get(s.key);
2434
+ const current = /* @__PURE__ */ new Map();
2435
+ const due = [];
2436
+ for (const runId of candidates) {
2437
+ const at = previous?.get(runId) ?? nowMs + Math.floor(random() * s.jitter);
2438
+ if (nowMs >= at) due.push(runId);
2439
+ else current.set(runId, at);
2440
+ }
2441
+ byKey.set(s.key, current);
2442
+ return due;
2443
+ }
2444
+ __name(jitterDue, "jitterDue");
2421
2445
  function scheduledRunId(key, everyMs, nowMs) {
2422
2446
  return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
2423
2447
  }
@@ -2487,13 +2511,19 @@ function scheduleRunIdsAt(s, nowMs) {
2487
2511
  __name(scheduleRunIdsAt, "scheduleRunIdsAt");
2488
2512
  async function runSchedules(engine, schedules, nowMs, opts) {
2489
2513
  const random = opts?.random ?? Math.random;
2490
- const sleep = opts?.sleep ?? defaultSleep;
2491
2514
  const settled = settledIdsFor(engine);
2492
2515
  const ids = [];
2493
2516
  for (const s of schedules) {
2494
2517
  if (engine.isDraining?.()) break;
2495
2518
  if (s.paused) continue;
2496
- const candidates = scheduleRunIdsAt(s, nowMs).filter((id) => !settled.get(s.key)?.has(id));
2519
+ const due = scheduleRunIdsAt(s, nowMs);
2520
+ const windows = s.jitter && s.jitter > 0 ? [
2521
+ .../* @__PURE__ */ new Set([
2522
+ ...deferredWindows(engine, s.key),
2523
+ ...due
2524
+ ])
2525
+ ] : due;
2526
+ const candidates = windows.filter((id) => !settled.get(s.key)?.has(id));
2497
2527
  if (candidates.length === 0) continue;
2498
2528
  if (s.overlap === "skip" && s.everyMs) {
2499
2529
  const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
@@ -2502,8 +2532,10 @@ async function runSchedules(engine, schedules, nowMs, opts) {
2502
2532
  if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
2503
2533
  }
2504
2534
  }
2505
- if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
2506
- for (const runId of candidates) {
2535
+ if (!(s.jitter && s.jitter > 0)) jitterFireAt.get(engine)?.delete(s.key);
2536
+ const firing = s.jitter && s.jitter > 0 ? jitterDue(engine, s, candidates, nowMs, random) : candidates;
2537
+ if (firing.length === 0) continue;
2538
+ for (const runId of firing) {
2507
2539
  if (engine.isDraining?.()) break;
2508
2540
  const existing = await engine.getRun(runId);
2509
2541
  remember(settled, s.key, runId);