dynamic-throttled-queue 2.3.0-rc.e78fc2f → 3.0.0-rc.1d87d99

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.
@@ -2,6 +2,8 @@ export function createPendingWork(options, host) {
2
2
  const { capacity, compactThreshold } = options;
3
3
  const queue = [];
4
4
  let head = 0;
5
+ /** Queued items that were removed but still sit between `head` and the tail; `take` skips them. */
6
+ const removed = new Set();
5
7
  const held = [];
6
8
  let frozen = false;
7
9
  let active = 0;
@@ -9,7 +11,7 @@ export function createPendingWork(options, host) {
9
11
  let deciding = 0;
10
12
  const idleWaiters = [];
11
13
  function queued() {
12
- return queue.length - head;
14
+ return queue.length - head - removed.size;
13
15
  }
14
16
  function pending() {
15
17
  return queued() + held.length;
@@ -44,6 +46,17 @@ export function createPendingWork(options, host) {
44
46
  held.length = 0;
45
47
  queue.length = 0;
46
48
  head = 0;
49
+ removed.clear();
50
+ }
51
+ function compact() {
52
+ if (head === queue.length) {
53
+ queue.length = 0;
54
+ head = 0;
55
+ }
56
+ else if (head > compactThreshold && head > queue.length / 2) {
57
+ queue.splice(0, head);
58
+ head = 0;
59
+ }
47
60
  }
48
61
  return {
49
62
  get queued() {
@@ -61,19 +74,24 @@ export function createPendingWork(options, host) {
61
74
  queue.push(item);
62
75
  },
63
76
  take() {
64
- if (head === queue.length)
65
- return undefined;
66
- const item = queue[head++];
67
- active++;
68
- if (head === queue.length) {
69
- queue.length = 0;
70
- head = 0;
71
- }
72
- else if (head > compactThreshold && head > queue.length / 2) {
73
- queue.splice(0, head);
74
- head = 0;
77
+ while (head < queue.length) {
78
+ const item = queue[head++];
79
+ if (removed.delete(item))
80
+ continue;
81
+ active++;
82
+ compact();
83
+ return item;
75
84
  }
76
- return item;
85
+ compact();
86
+ return undefined;
87
+ },
88
+ remove(item) {
89
+ const index = held.findIndex(retry => retry.item === item);
90
+ if (index === -1)
91
+ removed.add(item);
92
+ else
93
+ clearTimeout(held.splice(index, 1)[0].timeout);
94
+ notifyIdle();
77
95
  },
78
96
  settle(decide, observe) {
79
97
  active--;
@@ -1 +1 @@
1
- {"version":3,"file":"pending-work.js","sourceRoot":"","sources":["../src/pending-work.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pending-work.js","sourceRoot":"","sources":["../src/pending-work.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { FailureOutcome } from "./adaptive-rate.js";
2
+ export type RetryBackoff = {
3
+ strategy: "fixed" | "linear" | "exponential";
4
+ baseDelay: number;
5
+ maxDelay?: number;
6
+ jitter?: number;
7
+ random?: () => number;
8
+ };
9
+ export type RetryClassifier = (outcome: FailureOutcome, attempt: number) => boolean;
10
+ export type RetryPolicyOptions = {
11
+ retry?: number;
12
+ retryBackoff?: RetryBackoff;
13
+ retryClassifier?: RetryClassifier;
14
+ };
15
+ /** How a failed attempt continues: `{}` retries now, `{ delay }` retries after `delay` ms. */
16
+ type RetryDecision = Readonly<{
17
+ delay?: number;
18
+ }>;
19
+ export type RetryPolicy = {
20
+ /** Decides whether failed attempt `attempt` (one-based) gets another attempt. `undefined` drops it. */
21
+ decide: (outcome: FailureOutcome, attempt: number) => RetryDecision | undefined;
22
+ };
23
+ /** Validates the retry options, then decides each failed attempt: drop, retry now, or retry after a delay. */
24
+ export declare function createRetryPolicy({ retry, retryBackoff, retryClassifier }: RetryPolicyOptions): RetryPolicy;
25
+ export {};
@@ -0,0 +1,73 @@
1
+ const strategies = { fixed: true, linear: true, exponential: true };
2
+ const retryNow = Object.freeze({});
3
+ function validate(retry, retryBackoff) {
4
+ if (!Number.isInteger(retry) || retry < 0) {
5
+ throw new Error("retry must be a non-negative integer");
6
+ }
7
+ if (retryBackoff === undefined)
8
+ return;
9
+ if (!Object.hasOwn(strategies, retryBackoff.strategy)) {
10
+ throw new Error("retryBackoff.strategy must be fixed, linear, or exponential");
11
+ }
12
+ if (!Number.isFinite(retryBackoff.baseDelay) || retryBackoff.baseDelay < 0) {
13
+ throw new Error("retryBackoff.baseDelay must be a finite non-negative number");
14
+ }
15
+ if (retryBackoff.maxDelay !== undefined && (!Number.isFinite(retryBackoff.maxDelay) || retryBackoff.maxDelay < 0)) {
16
+ throw new Error("retryBackoff.maxDelay must be a finite non-negative number");
17
+ }
18
+ if (retryBackoff.jitter !== undefined && (!Number.isFinite(retryBackoff.jitter) || retryBackoff.jitter < 0 || retryBackoff.jitter > 1)) {
19
+ throw new Error("retryBackoff.jitter must be a finite number from 0 through 1");
20
+ }
21
+ }
22
+ function readRandom(random) {
23
+ try {
24
+ // eslint-disable-next-line sonarjs/pseudo-random -- Default jitter requires a random source.
25
+ const value = random?.() ?? Math.random();
26
+ return Number.isFinite(value) && value >= 0 && value <= 1 ? value : undefined;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ function calculateRetryDelay(policy, retryIndex) {
33
+ let delay;
34
+ switch (policy.strategy) {
35
+ case "linear":
36
+ delay = policy.baseDelay * retryIndex;
37
+ break;
38
+ case "exponential":
39
+ delay = policy.baseDelay * 2 ** (retryIndex - 1);
40
+ break;
41
+ default: delay = policy.baseDelay;
42
+ }
43
+ const cappedDelay = policy.maxDelay === undefined ? delay : Math.min(delay, policy.maxDelay);
44
+ if (policy.jitter === undefined)
45
+ return cappedDelay;
46
+ const random = readRandom(policy.random);
47
+ if (random === undefined)
48
+ return cappedDelay;
49
+ const jitteredDelay = cappedDelay * (1 + (random * 2 - 1) * policy.jitter);
50
+ return policy.maxDelay === undefined ? jitteredDelay : Math.min(jitteredDelay, policy.maxDelay);
51
+ }
52
+ /** Validates the retry options, then decides each failed attempt: drop, retry now, or retry after a delay. */
53
+ export function createRetryPolicy({ retry = 0, retryBackoff, retryClassifier }) {
54
+ validate(retry, retryBackoff);
55
+ function isRetryable(outcome, attempt) {
56
+ if (!retryClassifier)
57
+ return true;
58
+ try {
59
+ return retryClassifier(outcome, attempt) === true;
60
+ }
61
+ catch {
62
+ return true;
63
+ }
64
+ }
65
+ return {
66
+ decide(outcome, attempt) {
67
+ if (attempt > retry || !isRetryable(outcome, attempt))
68
+ return undefined;
69
+ return retryBackoff === undefined ? retryNow : { delay: calculateRetryDelay(retryBackoff, attempt) };
70
+ },
71
+ };
72
+ }
73
+ //# sourceMappingURL=retry-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-policy.js","sourceRoot":"","sources":["../src/retry-policy.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,GAAqD,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;AACtH,MAAM,QAAQ,GAAkB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAElD,SAAS,QAAQ,CAAC,KAAa,EAAE,YAAsC;IACrE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO;IACvC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,YAAY,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,YAAY,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC;QAClH,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,IAAI,YAAY,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QACvI,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAClF,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAkC;IACpD,IAAI,CAAC;QACH,6FAA6F;QAC7F,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1C,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAChF,CAAC;IACD,MAAM,CAAC;QACL,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,MAAoB,EAAE,UAAkB;IACnE,IAAI,KAAa,CAAC;IAClB,QAAQ,MAAM,CAAC,QAAQ,EAAE,CAAC;QACxB,KAAK,QAAQ;YAAE,KAAK,GAAG,MAAM,CAAC,SAAS,GAAG,UAAU,CAAC;YAAC,MAAM;QAC5D,KAAK,aAAa;YAAE,KAAK,GAAG,MAAM,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;YAAC,MAAM;QAC5E,OAAO,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC;IACpC,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7F,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,WAAW,CAAC;IACpD,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,WAAW,CAAC;IAC7C,MAAM,aAAa,GAAG,WAAW,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC3E,OAAO,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;AAClG,CAAC;AAED,8GAA8G;AAC9G,MAAM,UAAU,iBAAiB,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,YAAY,EAAE,eAAe,EAAsB;IAChG,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAE9B,SAAS,WAAW,CAAC,OAAuB,EAAE,OAAe;QAC3D,IAAI,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC;QAClC,IAAI,CAAC;YACH,OAAO,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QACpD,CAAC;QACD,MAAM,CAAC;YACL,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM,CAAC,OAAO,EAAE,OAAO;YACrB,IAAI,OAAO,GAAG,KAAK,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC;gBAAE,OAAO,SAAS,CAAC;YACxE,OAAO,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,mBAAmB,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,CAAC;QACvG,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,3 +1,62 @@
1
1
  import type { AdaptiveRateOptions } from "./adaptive-rate.js";
2
- import type { ThrottleHandle, ThrottleOptions } from "./dynamic-throttled-queue.js";
3
- export declare function createScheduler(options: ThrottleOptions, adaptiveRateOptions: AdaptiveRateOptions): ThrottleHandle;
2
+ import type { PendingWorkOptions } from "./pending-work.js";
3
+ import type { RetryPolicy } from "./retry-policy.js";
4
+ export type ExecutionContext = Readonly<{
5
+ signal: AbortSignal;
6
+ /** One-based attempt number; retries of the same item count up from 1. */
7
+ attempt: number;
8
+ }>;
9
+ /** Return `false` to signal failure (counts as an error, retries if configured). Any other value is success. */
10
+ export type ThrottleCallback = (context: ExecutionContext) => unknown;
11
+ export type ThrottleFn = (callback: ThrottleCallback) => void;
12
+ /** Submitted work. Every returned value, including `false`, is a result; only a throw or rejection fails. */
13
+ export type TaskCallback<T> = (context: ExecutionContext) => T | Promise<T>;
14
+ /** One accepted logical item across all of its attempts. */
15
+ export type TaskHandle<T> = Readonly<{
16
+ /** Settles once: the final value, the final failure, or the cancellation reason. */
17
+ result: Promise<T>;
18
+ /**
19
+ * Rejects `result` with `reason` (an `AbortError` by default) unless it already settled. A pending
20
+ * item is removed; an active one has its signal aborted and is never retried. Repeat calls do nothing.
21
+ */
22
+ cancel: (reason?: unknown) => void;
23
+ }>;
24
+ export type QueueLifecycleState = "running" | "paused" | "stopped" | "aborted" | "failed";
25
+ export type QueueState = Readonly<{
26
+ rate: number;
27
+ pending: number;
28
+ active: number;
29
+ state: QueueLifecycleState;
30
+ started: number;
31
+ succeeded: number;
32
+ failed: number;
33
+ retried: number;
34
+ /** Submitted items whose result was rejected by `cancel()`. */
35
+ canceled: number;
36
+ rateIncreases: number;
37
+ rateDecreases: number;
38
+ /** Milliseconds, rounded up, until a server-directed cooldown ends; `0` when none is active. */
39
+ cooldownRemaining: number;
40
+ /** `cooldownFor()` calls that moved the cooldown deadline later. */
41
+ cooldowns: number;
42
+ /** Total milliseconds those calls added to the cooldown deadline. */
43
+ cooldownTotal: number;
44
+ }>;
45
+ export type ThrottleHandle = ThrottleFn & {
46
+ submit: <T>(callback: TaskCallback<T>) => TaskHandle<T>;
47
+ pause: () => void;
48
+ resume: () => void;
49
+ stop: () => void;
50
+ abort: () => void;
51
+ /** Starts nothing new for `delay` ms (capped at `maxCooldown`); never shortens an active cooldown. */
52
+ cooldownFor: (delay: number) => void;
53
+ waitForIdle: () => Promise<void>;
54
+ getState: () => QueueState;
55
+ };
56
+ export type SchedulerOptions = PendingWorkOptions & {
57
+ /** Most callbacks awaiting asynchronous settlement at once. */
58
+ concurrency: number;
59
+ /** Longest cooldown, in ms, that `cooldownFor()` applies; longer requests are clamped to it. */
60
+ maxCooldown: number;
61
+ };
62
+ export declare function createScheduler(options: SchedulerOptions, adaptiveRateOptions: AdaptiveRateOptions, retryPolicy: RetryPolicy): ThrottleHandle;
package/dist/scheduler.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createAdaptiveRate } from "./adaptive-rate.js";
2
2
  import { createPendingWork } from "./pending-work.js";
3
- import { calculateRetryDelay } from "./retry-backoff.js";
3
+ const returnedFalse = Object.freeze({ kind: "returned-false" });
4
4
  /** The state each event leads to. An event missing from the current state's row is a no-op. */
5
5
  const lifecycleTransitions = {
6
6
  running: { pause: "paused", stop: "stopped", abort: "aborted", fail: "failed" },
@@ -9,40 +9,40 @@ const lifecycleTransitions = {
9
9
  aborted: {},
10
10
  failed: {},
11
11
  };
12
- export function createScheduler(options, adaptiveRateOptions) {
13
- const { interval, evenly_spaced = true, retry = 0, retryBackoff, concurrency, maxQueueSize, compact_threshold = 512, retryClassifier, } = options;
14
- let last_called = 0;
15
- let timeout;
12
+ export function createScheduler(options, adaptiveRateOptions, retryPolicy) {
13
+ const { concurrency, maxCooldown, ...pendingWorkOptions } = options;
16
14
  let lifecycle = { state: "running" };
17
15
  let cnt_started = 0;
18
16
  let cnt_succeeded = 0;
19
17
  let cnt_failed = 0;
20
18
  let cnt_retried = 0;
19
+ let cnt_canceled = 0;
20
+ let cnt_cooldowns = 0;
21
+ let cooldownTotal = 0;
22
+ /** Monotonic (`performance.now()`) time before which nothing starts; `undefined` when no cooldown is active. */
23
+ let cooldownDeadline;
24
+ /** Fires at the deadline. Cleared while stopped, which keeps the deadline for the restart. */
25
+ let cooldownTimer;
21
26
  const abortController = new AbortController();
22
- const max_concurrency = concurrency ?? Infinity;
23
- const work = createPendingWork({ capacity: maxQueueSize ?? Infinity, compactThreshold: compact_threshold }, { retryQueued: start });
27
+ /** Submitted items whose result has not settled. */
28
+ const tasks = new Set();
29
+ const work = createPendingWork(pendingWorkOptions, { retryQueued: start });
24
30
  const adaptiveRate = createAdaptiveRate(adaptiveRateOptions, {
25
31
  hasPendingWork: () => work.queued > 0,
26
- resumeStarts() {
27
- last_called = Date.now();
28
- clearTimeout(timeout);
29
- timeout = setTimeout(dequeue, spacing());
30
- },
31
- holdStarts(deferBy) {
32
- clearTimeout(timeout);
33
- timeout = deferBy === undefined ? undefined : setTimeout(dequeue, spacing() + deferBy);
34
- },
35
- idle() {
36
- clearTimeout(timeout);
37
- timeout = undefined;
32
+ startsDue(limit) {
33
+ const batch = Math.min(limit, work.queued);
34
+ for (let started = 0; started < batch && work.active < concurrency && cooldownDeadline === undefined; started++) {
35
+ const item = work.take();
36
+ if (item === undefined)
37
+ break;
38
+ execute(item);
39
+ }
40
+ return work.active < concurrency;
38
41
  },
39
42
  failed(error) {
40
43
  transition("fail", error);
41
44
  },
42
45
  });
43
- function spacing() {
44
- return evenly_spaced ? interval / adaptiveRate.rate : interval;
45
- }
46
46
  /** Moves the lifecycle for `event` and runs the cleanup that entering the new state requires. */
47
47
  function transition(event, error) {
48
48
  const next = lifecycleTransitions[lifecycle.state][event];
@@ -52,120 +52,205 @@ export function createScheduler(options, adaptiveRateOptions) {
52
52
  switch (next) {
53
53
  case "running":
54
54
  work.thaw();
55
- adaptiveRate.start();
55
+ if (cooldownDeadline === undefined) {
56
+ adaptiveRate.start();
57
+ return;
58
+ }
59
+ armCooldown(cooldownDeadline);
60
+ adaptiveRate.suspend();
56
61
  return;
57
62
  case "paused":
63
+ // A cooldown keeps elapsing while paused: its deadline is the server's, not the queue's.
58
64
  work.freeze();
59
65
  adaptiveRate.pause();
60
66
  return;
61
67
  case "stopped":
68
+ clearTimeout(cooldownTimer);
69
+ cooldownTimer = undefined;
62
70
  work.freeze();
63
71
  adaptiveRate.stop();
64
72
  return;
65
73
  case "aborted":
74
+ clearCooldown();
66
75
  adaptiveRate.stop();
67
76
  work.discard();
68
77
  abortController.abort();
78
+ rejectTasks(abortController.signal.reason);
69
79
  return;
70
80
  case "failed":
81
+ clearCooldown();
71
82
  adaptiveRate.stop();
72
83
  work.fail(error);
84
+ rejectTasks(error);
73
85
  }
74
86
  }
75
- function isRetryable(item, outcome) {
76
- if (!outcome || item.retries === 0)
77
- return false;
78
- try {
79
- return retryClassifier ? retryClassifier(outcome, retry - item.retries + 1) === true : true;
87
+ function armCooldown(deadline) {
88
+ clearTimeout(cooldownTimer);
89
+ cooldownTimer = setTimeout(expireCooldown, Math.max(0, deadline - performance.now()));
90
+ }
91
+ function expireCooldown() {
92
+ // A timer can fire slightly before the monotonic deadline; wait out the remainder.
93
+ if (cooldownDeadline !== undefined && performance.now() < cooldownDeadline) {
94
+ armCooldown(cooldownDeadline);
95
+ return;
80
96
  }
81
- catch {
82
- return true;
97
+ cooldownTimer = undefined;
98
+ cooldownDeadline = undefined;
99
+ start();
100
+ }
101
+ function clearCooldown() {
102
+ clearTimeout(cooldownTimer);
103
+ cooldownTimer = undefined;
104
+ cooldownDeadline = undefined;
105
+ }
106
+ /** Rejects every unsettled submitted item with `reason` and aborts its signal. */
107
+ function rejectTasks(reason) {
108
+ // Snapshot first: abort listeners run synchronously and may re-enter the queue.
109
+ const affected = [...tasks];
110
+ tasks.clear();
111
+ for (const task of affected) {
112
+ task.controller.abort(reason);
113
+ task.reject(reason);
83
114
  }
84
115
  }
85
- function nextAttempt(item, outcome) {
86
- if (!isRetryable(item, outcome))
87
- return undefined;
88
- cnt_retried++;
89
- const retryItem = { fn: item.fn, retries: item.retries - 1 };
90
- if (retryBackoff === undefined)
91
- return { item: retryItem };
92
- return { item: retryItem, delay: calculateRetryDelay(retryBackoff, retry - item.retries + 1) };
116
+ function nextAttempt(item, outcome, value) {
117
+ const { task } = item;
118
+ if (outcome) {
119
+ const decision = retryPolicy.decide(outcome, item.attempt);
120
+ if (decision && (task === undefined || tasks.has(task))) {
121
+ cnt_retried++;
122
+ item.attempt++;
123
+ if (task)
124
+ task.active = false;
125
+ return { item, ...decision };
126
+ }
127
+ }
128
+ if (task && tasks.delete(task)) {
129
+ // Submitted items never return-false, so a failure always carries its error.
130
+ if (outcome)
131
+ task.reject("error" in outcome ? outcome.error : undefined);
132
+ else
133
+ task.resolve(value);
134
+ }
135
+ return undefined;
93
136
  }
94
- function handleSettlement(item, outcome, reportSettlement, resume = false) {
137
+ function handleSettlement(item, outcome, value, reportSettlement, resume = false) {
95
138
  if (lifecycle.state === "aborted" || lifecycle.state === "failed") {
96
139
  work.settle();
97
140
  return;
98
141
  }
142
+ const observe = () => {
143
+ reportSettlement(outcome);
144
+ // An async settlement frees a slot; a start may be due if concurrency held one back.
145
+ if (resume)
146
+ start();
147
+ };
148
+ if (item.task && !tasks.has(item.task)) {
149
+ // Canceled: the attempt's outcome neither counts nor retries, but the observation window still closes.
150
+ outcome = undefined;
151
+ work.settle(undefined, observe);
152
+ return;
153
+ }
99
154
  if (outcome)
100
155
  cnt_failed++;
101
156
  else
102
157
  cnt_succeeded++;
103
- work.settle(() => nextAttempt(item, outcome), () => {
104
- reportSettlement(outcome);
105
- if (resume && adaptiveRate.pacing === "open" && work.queued > 0)
106
- dequeue();
107
- });
158
+ work.settle(() => nextAttempt(item, outcome, value), observe);
159
+ }
160
+ /** Only fire-and-forget callbacks report failure by returning `false`. */
161
+ function returnedOutcome(item, value) {
162
+ return value === false && item.task === undefined ? returnedFalse : undefined;
108
163
  }
109
164
  function execute(item) {
110
165
  const reportSettlement = adaptiveRate.started();
111
166
  cnt_started++;
167
+ const { task } = item;
168
+ if (task)
169
+ task.active = true;
112
170
  let result;
113
171
  try {
114
- result = item.fn({ signal: abortController.signal });
172
+ result = item.fn({ signal: task?.controller.signal ?? abortController.signal, attempt: item.attempt });
115
173
  }
116
174
  catch (error) {
117
- handleSettlement(item, { kind: "thrown", error }, reportSettlement);
175
+ handleSettlement(item, { kind: "thrown", error }, undefined, reportSettlement);
118
176
  return;
119
177
  }
120
178
  if (result instanceof Promise) {
121
- void result.then(value => handleSettlement(item, value === false ? { kind: "returned-false" } : undefined, reportSettlement, true), (error) => handleSettlement(item, { kind: "rejected", error }, reportSettlement, true));
179
+ void result.then((value) => handleSettlement(item, returnedOutcome(item, value), value, reportSettlement, true), (error) => handleSettlement(item, { kind: "rejected", error }, undefined, reportSettlement, true));
122
180
  return;
123
181
  }
124
- handleSettlement(item, result === false ? { kind: "returned-false" } : undefined, reportSettlement);
125
- }
126
- function dequeue() {
127
- const threshold = last_called + spacing();
128
- const now = Date.now();
129
- if (now < threshold) {
130
- clearTimeout(timeout);
131
- timeout = setTimeout(dequeue, threshold - now);
132
- return;
133
- }
134
- const batch = Math.min(evenly_spaced ? 1 : adaptiveRate.rate, work.queued);
135
- let started = 0;
136
- while (started < batch && work.active < max_concurrency) {
137
- const item = work.take();
138
- if (item === undefined)
139
- break;
140
- if (started++ === 0)
141
- last_called = Date.now();
142
- execute(item);
143
- }
144
- if (work.queued === 0)
145
- adaptiveRate.drained();
146
- if (adaptiveRate.pacing !== "open" || work.active >= max_concurrency)
147
- return;
148
- timeout = setTimeout(dequeue, spacing());
182
+ handleSettlement(item, returnedOutcome(item, result), result, reportSettlement);
149
183
  }
150
184
  function start() {
151
- if (lifecycle.state === "running")
185
+ if (lifecycle.state === "running" && cooldownDeadline === undefined)
152
186
  adaptiveRate.start();
153
187
  }
154
- function enqueue(callback) {
188
+ /** Admits `item` or throws: the queue is terminal or every capacity reservation is taken. */
189
+ function accept(item) {
155
190
  if (lifecycle.state === "aborted")
156
191
  throw new Error("Cannot enqueue work after the queue has been aborted");
157
192
  if (lifecycle.state === "failed")
158
193
  throw lifecycle.error;
159
- work.accept({ fn: callback, retries: retry });
194
+ work.accept(item);
195
+ }
196
+ /** Schedules newly accepted work. */
197
+ function schedule() {
160
198
  if (lifecycle.state === "stopped")
161
199
  transition("restart");
162
200
  else
163
201
  start();
164
202
  }
203
+ function enqueue(callback) {
204
+ accept({ fn: callback, attempt: 1 });
205
+ schedule();
206
+ }
207
+ enqueue.submit = (callback) => {
208
+ const { promise, resolve, reject } = Promise.withResolvers();
209
+ const task = { controller: new AbortController(), resolve: resolve, reject, active: false };
210
+ const item = { fn: callback, attempt: 1, task };
211
+ accept(item);
212
+ tasks.add(task);
213
+ schedule();
214
+ return {
215
+ result: promise,
216
+ cancel(reason) {
217
+ if (!tasks.delete(task))
218
+ return;
219
+ cnt_canceled++;
220
+ if (!task.active)
221
+ work.remove(item);
222
+ task.controller.abort(reason);
223
+ task.reject(task.controller.signal.reason);
224
+ },
225
+ };
226
+ };
165
227
  enqueue.pause = () => transition("pause");
166
228
  enqueue.resume = () => transition("resume");
167
229
  enqueue.stop = () => transition("stop");
168
230
  enqueue.abort = () => transition("abort");
231
+ enqueue.cooldownFor = (delay) => {
232
+ if (!Number.isFinite(delay) || delay < 0)
233
+ throw new RangeError("cooldownFor delay must be a finite non-negative number");
234
+ if (lifecycle.state === "aborted" || lifecycle.state === "failed")
235
+ return;
236
+ const now = performance.now();
237
+ const latest = Math.max(now, cooldownDeadline ?? now);
238
+ const deadline = now + Math.min(delay, maxCooldown);
239
+ // Hints are absolute ("not before now + delay"), so overlapping ones keep the later deadline, never add up.
240
+ if (deadline <= latest)
241
+ return;
242
+ const beginning = cooldownDeadline === undefined;
243
+ cnt_cooldowns++;
244
+ cooldownTotal += deadline - latest;
245
+ cooldownDeadline = deadline;
246
+ // Stopped: the restart arms the timer for whatever remains.
247
+ if (lifecycle.state === "stopped")
248
+ return;
249
+ armCooldown(deadline);
250
+ // Paused: adaptive rate is already stopped; `resume()` suspends it instead of starting it.
251
+ if (beginning && lifecycle.state === "running")
252
+ adaptiveRate.suspend();
253
+ };
169
254
  enqueue.waitForIdle = async () => {
170
255
  if (lifecycle.state === "failed")
171
256
  throw lifecycle.error;
@@ -180,10 +265,13 @@ export function createScheduler(options, adaptiveRateOptions) {
180
265
  succeeded: cnt_succeeded,
181
266
  failed: cnt_failed,
182
267
  retried: cnt_retried,
268
+ canceled: cnt_canceled,
183
269
  rateIncreases: adaptiveRate.rateIncreases,
184
270
  rateDecreases: adaptiveRate.rateDecreases,
271
+ cooldownRemaining: cooldownDeadline === undefined ? 0 : Math.max(0, Math.ceil(cooldownDeadline - performance.now())),
272
+ cooldowns: cnt_cooldowns,
273
+ cooldownTotal,
185
274
  });
186
- Object.defineProperty(enqueue, "pending", { get: () => work.pending });
187
275
  return enqueue;
188
276
  }
189
277
  //# sourceMappingURL=scheduler.js.map