@adaptive-concurrency-toolkit/core 1.0.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/clock.d.ts +20 -0
  4. package/dist/clock.js +23 -0
  5. package/dist/clock.js.map +1 -0
  6. package/dist/index.d.ts +10 -0
  7. package/dist/index.js +8 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/limit/aimd-limit.d.ts +51 -0
  10. package/dist/limit/aimd-limit.js +75 -0
  11. package/dist/limit/aimd-limit.js.map +1 -0
  12. package/dist/limit/fixed-limit.d.ts +17 -0
  13. package/dist/limit/fixed-limit.js +22 -0
  14. package/dist/limit/fixed-limit.js.map +1 -0
  15. package/dist/limit/gradient2-limit.d.ts +80 -0
  16. package/dist/limit/gradient2-limit.js +139 -0
  17. package/dist/limit/gradient2-limit.js.map +1 -0
  18. package/dist/limit/index.d.ts +5 -0
  19. package/dist/limit/index.js +5 -0
  20. package/dist/limit/index.js.map +1 -0
  21. package/dist/limit/limit.d.ts +30 -0
  22. package/dist/limit/limit.js +2 -0
  23. package/dist/limit/limit.js.map +1 -0
  24. package/dist/limit/vegas-limit.d.ts +80 -0
  25. package/dist/limit/vegas-limit.js +137 -0
  26. package/dist/limit/vegas-limit.js.map +1 -0
  27. package/dist/limiter/index.d.ts +2 -0
  28. package/dist/limiter/index.js +2 -0
  29. package/dist/limiter/index.js.map +1 -0
  30. package/dist/limiter/limiter.d.ts +18 -0
  31. package/dist/limiter/limiter.js +2 -0
  32. package/dist/limiter/limiter.js.map +1 -0
  33. package/dist/limiter/simple-limiter.d.ts +28 -0
  34. package/dist/limiter/simple-limiter.js +70 -0
  35. package/dist/limiter/simple-limiter.js.map +1 -0
  36. package/dist/types.d.ts +27 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/util/ema.d.ts +27 -0
  40. package/dist/util/ema.js +53 -0
  41. package/dist/util/ema.js.map +1 -0
  42. package/dist/util/listeners.d.ts +12 -0
  43. package/dist/util/listeners.js +49 -0
  44. package/dist/util/listeners.js.map +1 -0
  45. package/dist/util/math.d.ts +1 -0
  46. package/dist/util/math.js +4 -0
  47. package/dist/util/math.js.map +1 -0
  48. package/package.json +54 -0
@@ -0,0 +1,139 @@
1
+ import { Ema } from "../util/ema.js";
2
+ import { clamp } from "../util/math.js";
3
+ import { ChangeListeners } from "../util/listeners.js";
4
+ const DEFAULT_QUEUE_SIZE = (limit) => 4 * Math.sqrt(limit);
5
+ /**
6
+ * Gradient2 — adaptive limit based on the ratio of long-window RTT baseline to
7
+ * the current short-window RTT.
8
+ *
9
+ * Per window:
10
+ *
11
+ * shortRtt = window min RTT
12
+ * longRtt = EMA over windows of shortRtt
13
+ * gradient = clamp(tolerance · longRtt / shortRtt, minGradient, 1)
14
+ * queue = queueSize(limit)
15
+ * newLimit = limit · gradient + queue
16
+ * if newLimit < limit: newLimit ← smoothing · newLimit + (1−smoothing) · limit
17
+ *
18
+ * The `gradient ≤ 1` constraint means we only grow via the queue-size hedge —
19
+ * `limit · gradient` alone can never increase the limit. This matches the
20
+ * intuition that a fast response window is permission to add a probe, not to
21
+ * scale up multiplicatively.
22
+ */
23
+ export class Gradient2Limit {
24
+ _limit;
25
+ minLimit;
26
+ maxLimit;
27
+ windowNanos;
28
+ minWindowSamples;
29
+ tolerance;
30
+ smoothing;
31
+ minGradient;
32
+ backoffRatio;
33
+ queueSizeFn;
34
+ longRtt;
35
+ listeners = new ChangeListeners();
36
+ windowStartNanos = -1;
37
+ windowMinRttNanos = Number.POSITIVE_INFINITY;
38
+ windowMaxInflight = 0;
39
+ windowSamples = 0;
40
+ windowDropped = false;
41
+ constructor(opts = {}) {
42
+ const initial = opts.initialLimit ?? 20;
43
+ const min = opts.minLimit ?? 1;
44
+ const max = opts.maxLimit ?? 1000;
45
+ if (!(min >= 1))
46
+ throw new RangeError(`minLimit must be >= 1`);
47
+ if (!(max >= min))
48
+ throw new RangeError(`maxLimit must be >= minLimit`);
49
+ if (!(initial >= min && initial <= max)) {
50
+ throw new RangeError(`initialLimit must be in [${min}, ${max}]`);
51
+ }
52
+ this._limit = initial;
53
+ this.minLimit = min;
54
+ this.maxLimit = max;
55
+ this.windowNanos = opts.windowNanos ?? 1_000_000_000;
56
+ this.minWindowSamples = opts.minWindowSamples ?? 10;
57
+ this.tolerance = opts.tolerance ?? 1.5;
58
+ this.smoothing = opts.smoothing ?? 0.2;
59
+ this.minGradient = opts.minGradient ?? 0.5;
60
+ this.backoffRatio = opts.backoffRatio ?? 0.9;
61
+ this.queueSizeFn = opts.queueSize ?? DEFAULT_QUEUE_SIZE;
62
+ this.longRtt = Ema.withWindow(opts.longWindowCount ?? 100);
63
+ if (!(this.tolerance > 0))
64
+ throw new RangeError(`tolerance must be > 0`);
65
+ if (!(this.smoothing > 0 && this.smoothing <= 1)) {
66
+ throw new RangeError(`smoothing must be in (0, 1]`);
67
+ }
68
+ if (!(this.minGradient > 0 && this.minGradient <= 1)) {
69
+ throw new RangeError(`minGradient must be in (0, 1]`);
70
+ }
71
+ if (!(this.backoffRatio > 0 && this.backoffRatio < 1)) {
72
+ throw new RangeError(`backoffRatio must be in (0, 1)`);
73
+ }
74
+ }
75
+ get limit() {
76
+ return this._limit;
77
+ }
78
+ /** Long-window RTT baseline (ns). 0 until the first window commits. */
79
+ get longRttNanos() {
80
+ return this.longRtt.value;
81
+ }
82
+ onSample(startNanos, rttNanos, inflight, didDrop) {
83
+ if (this.windowStartNanos < 0)
84
+ this.windowStartNanos = startNanos;
85
+ if (rttNanos < this.windowMinRttNanos)
86
+ this.windowMinRttNanos = rttNanos;
87
+ if (inflight > this.windowMaxInflight)
88
+ this.windowMaxInflight = inflight;
89
+ if (didDrop)
90
+ this.windowDropped = true;
91
+ this.windowSamples++;
92
+ const elapsed = startNanos - this.windowStartNanos;
93
+ if (elapsed >= this.windowNanos && this.windowSamples >= this.minWindowSamples) {
94
+ this.commitWindow();
95
+ }
96
+ }
97
+ onChange(listener) {
98
+ return this.listeners.add(listener);
99
+ }
100
+ commitWindow() {
101
+ const current = this._limit;
102
+ const shortRtt = this.windowMinRttNanos;
103
+ const maxInflight = this.windowMaxInflight;
104
+ const dropped = this.windowDropped;
105
+ this.windowStartNanos = -1;
106
+ this.windowMinRttNanos = Number.POSITIVE_INFINITY;
107
+ this.windowMaxInflight = 0;
108
+ this.windowSamples = 0;
109
+ this.windowDropped = false;
110
+ let next;
111
+ if (dropped) {
112
+ next = Math.max(this.minLimit, Math.floor(current * this.backoffRatio));
113
+ }
114
+ else {
115
+ // Skip the very first window — the long-RTT EMA seeds from it, so the
116
+ // gradient would be 1.0 trivially with no real signal yet.
117
+ const wasInitialized = this.longRtt.initialized;
118
+ const longRtt = this.longRtt.update(shortRtt);
119
+ if (!wasInitialized)
120
+ return;
121
+ const gradient = clamp((this.tolerance * longRtt) / shortRtt, this.minGradient, 1);
122
+ const queue = this.queueSizeFn(current);
123
+ let candidate = current * gradient + queue;
124
+ // Only grow when we are actually using the limit; otherwise hold.
125
+ if (candidate > current && maxInflight * 2 < current)
126
+ return;
127
+ if (candidate < current) {
128
+ candidate = this.smoothing * candidate + (1 - this.smoothing) * current;
129
+ }
130
+ next = candidate;
131
+ }
132
+ next = Math.round(clamp(next, this.minLimit, this.maxLimit));
133
+ if (next !== current) {
134
+ this._limit = next;
135
+ this.listeners.emit(next);
136
+ }
137
+ }
138
+ }
139
+ //# sourceMappingURL=gradient2-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gradient2-limit.js","sourceRoot":"","sources":["../../src/limit/gradient2-limit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export type { Limit } from './limit.ts';
2
+ export { FixedLimit, type FixedLimitOptions } from './fixed-limit.ts';
3
+ export { AimdLimit, type AimdLimitOptions } from './aimd-limit.ts';
4
+ export { VegasLimit, type VegasLimitOptions } from './vegas-limit.ts';
5
+ export { Gradient2Limit, type Gradient2LimitOptions } from './gradient2-limit.ts';
@@ -0,0 +1,5 @@
1
+ export { FixedLimit } from "./fixed-limit.js";
2
+ export { AimdLimit } from "./aimd-limit.js";
3
+ export { VegasLimit } from "./vegas-limit.js";
4
+ export { Gradient2Limit } from "./gradient2-limit.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/limit/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAA0B,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,UAAU,EAA0B,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,cAAc,EAA8B,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { LimitChangeListener, Nanos, Unsubscribe } from '../types.ts';
2
+ /**
3
+ * Concurrency limit algorithm. Implementations consume completion samples and
4
+ * expose the current inferred ceiling on in-flight work.
5
+ *
6
+ * The {@link onSample} signature is intentionally positional so the call site
7
+ * never allocates a sample object on the hot path.
8
+ */
9
+ export interface Limit {
10
+ /** Current inferred concurrency ceiling. Always >= 1. */
11
+ readonly limit: number;
12
+ /**
13
+ * Report a completed operation.
14
+ *
15
+ * @param startTimeNanos Monotonic timestamp when the permit was issued.
16
+ * Algorithms may use it for windowing.
17
+ * @param rttNanos Observed round-trip time (now − startTimeNanos).
18
+ * @param inflight In-flight count when the permit was issued
19
+ * (or peak in-flight observed during the sample's lifetime — see limiter).
20
+ * @param didDrop True if the caller reported the request as dropped
21
+ * (timeout, 5xx, overload). Algorithms typically treat this as the
22
+ * strongest decrease signal.
23
+ */
24
+ onSample(startTimeNanos: Nanos, rttNanos: Nanos, inflight: number, didDrop: boolean): void;
25
+ /**
26
+ * Subscribe to limit changes. Returns an unsubscribe function. The callback
27
+ * is invoked synchronously from `onSample` when the limit changes.
28
+ */
29
+ onChange(listener: LimitChangeListener): Unsubscribe;
30
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limit.js","sourceRoot":"","sources":["../../src/limit/limit.ts"],"names":[],"mappings":""}
@@ -0,0 +1,80 @@
1
+ import type { LimitChangeListener, Nanos, Unsubscribe } from '../types.ts';
2
+ import type { Limit } from './limit.ts';
3
+ export interface VegasLimitOptions {
4
+ readonly initialLimit?: number;
5
+ readonly minLimit?: number;
6
+ readonly maxLimit?: number;
7
+ /**
8
+ * Window duration in nanoseconds. Samples within a window are aggregated and
9
+ * the limit is updated at most once per window. Defaults to 1 s.
10
+ */
11
+ readonly windowNanos?: number;
12
+ /** Minimum samples required to update at the end of a window. Defaults to 10. */
13
+ readonly minWindowSamples?: number;
14
+ /**
15
+ * Lower queue-size target as a function of limit. Default: `3·log10(limit)`.
16
+ * Below this, the limit is increased.
17
+ */
18
+ readonly alpha?: (limit: number) => number;
19
+ /**
20
+ * Upper queue-size target as a function of limit. Default: `6·log10(limit)`.
21
+ * Above this, the limit is decreased.
22
+ */
23
+ readonly beta?: (limit: number) => number;
24
+ /** Increase step. Default: `log10(limit)`. */
25
+ readonly increase?: (limit: number) => number;
26
+ /** Decrease step. Default: `log10(limit)`. */
27
+ readonly decrease?: (limit: number) => number;
28
+ /** Multiplicative back-off on a drop sample. Default 0.9. */
29
+ readonly backoffRatio?: number;
30
+ /**
31
+ * After this many windows without observing a new minimum RTT, the noload
32
+ * estimate is reset to the current window's min RTT — re-probing the floor.
33
+ * Default 10.
34
+ */
35
+ readonly probeIntervalWindows?: number;
36
+ }
37
+ /**
38
+ * TCP-Vegas inspired adaptive limit.
39
+ *
40
+ * Per window (≈ 1 s by default) compute:
41
+ *
42
+ * queue = limit · (1 − rttNoLoad / rttWindowMin)
43
+ *
44
+ * and adjust:
45
+ *
46
+ * queue ≤ α(limit) → grow (limit + log10 limit)
47
+ * queue ≥ β(limit) → shrink
48
+ * else → hold
49
+ *
50
+ * `rttNoLoad` is the rolling minimum RTT, periodically re-probed so the floor
51
+ * adapts to legitimate baseline shifts.
52
+ */
53
+ export declare class VegasLimit implements Limit {
54
+ private _limit;
55
+ readonly minLimit: number;
56
+ readonly maxLimit: number;
57
+ readonly windowNanos: number;
58
+ readonly minWindowSamples: number;
59
+ readonly backoffRatio: number;
60
+ readonly probeIntervalWindows: number;
61
+ private readonly alphaFn;
62
+ private readonly betaFn;
63
+ private readonly increaseFn;
64
+ private readonly decreaseFn;
65
+ private readonly listeners;
66
+ private rttNoLoadNanos;
67
+ private windowStartNanos;
68
+ private windowMinRttNanos;
69
+ private windowMaxInflight;
70
+ private windowSamples;
71
+ private windowDropped;
72
+ private windowsSinceNewMin;
73
+ constructor(opts?: VegasLimitOptions);
74
+ get limit(): number;
75
+ /** Current rtt-no-load estimate (ns). `+Infinity` until the first sample. */
76
+ get rttNoLoad(): number;
77
+ onSample(startNanos: Nanos, rttNanos: Nanos, inflight: number, didDrop: boolean): void;
78
+ onChange(listener: LimitChangeListener): Unsubscribe;
79
+ private commitWindow;
80
+ }
@@ -0,0 +1,137 @@
1
+ import { clamp } from "../util/math.js";
2
+ import { ChangeListeners } from "../util/listeners.js";
3
+ const DEFAULT_ALPHA = (limit) => 3 * Math.log10(limit);
4
+ const DEFAULT_BETA = (limit) => 6 * Math.log10(limit);
5
+ const DEFAULT_STEP = (limit) => Math.log10(limit);
6
+ /**
7
+ * TCP-Vegas inspired adaptive limit.
8
+ *
9
+ * Per window (≈ 1 s by default) compute:
10
+ *
11
+ * queue = limit · (1 − rttNoLoad / rttWindowMin)
12
+ *
13
+ * and adjust:
14
+ *
15
+ * queue ≤ α(limit) → grow (limit + log10 limit)
16
+ * queue ≥ β(limit) → shrink
17
+ * else → hold
18
+ *
19
+ * `rttNoLoad` is the rolling minimum RTT, periodically re-probed so the floor
20
+ * adapts to legitimate baseline shifts.
21
+ */
22
+ export class VegasLimit {
23
+ _limit;
24
+ minLimit;
25
+ maxLimit;
26
+ windowNanos;
27
+ minWindowSamples;
28
+ backoffRatio;
29
+ probeIntervalWindows;
30
+ alphaFn;
31
+ betaFn;
32
+ increaseFn;
33
+ decreaseFn;
34
+ listeners = new ChangeListeners();
35
+ rttNoLoadNanos = Number.POSITIVE_INFINITY;
36
+ windowStartNanos = -1;
37
+ windowMinRttNanos = Number.POSITIVE_INFINITY;
38
+ windowMaxInflight = 0;
39
+ windowSamples = 0;
40
+ windowDropped = false;
41
+ windowsSinceNewMin = 0;
42
+ constructor(opts = {}) {
43
+ const initial = opts.initialLimit ?? 20;
44
+ const min = opts.minLimit ?? 1;
45
+ const max = opts.maxLimit ?? 1000;
46
+ if (!(min >= 1))
47
+ throw new RangeError(`minLimit must be >= 1`);
48
+ if (!(max >= min))
49
+ throw new RangeError(`maxLimit must be >= minLimit`);
50
+ if (!(initial >= min && initial <= max)) {
51
+ throw new RangeError(`initialLimit must be in [${min}, ${max}]`);
52
+ }
53
+ this._limit = initial;
54
+ this.minLimit = min;
55
+ this.maxLimit = max;
56
+ this.windowNanos = opts.windowNanos ?? 1_000_000_000;
57
+ this.minWindowSamples = opts.minWindowSamples ?? 10;
58
+ this.backoffRatio = opts.backoffRatio ?? 0.9;
59
+ this.probeIntervalWindows = opts.probeIntervalWindows ?? 10;
60
+ this.alphaFn = opts.alpha ?? DEFAULT_ALPHA;
61
+ this.betaFn = opts.beta ?? DEFAULT_BETA;
62
+ this.increaseFn = opts.increase ?? DEFAULT_STEP;
63
+ this.decreaseFn = opts.decrease ?? DEFAULT_STEP;
64
+ }
65
+ get limit() {
66
+ return this._limit;
67
+ }
68
+ /** Current rtt-no-load estimate (ns). `+Infinity` until the first sample. */
69
+ get rttNoLoad() {
70
+ return this.rttNoLoadNanos;
71
+ }
72
+ onSample(startNanos, rttNanos, inflight, didDrop) {
73
+ if (this.windowStartNanos < 0)
74
+ this.windowStartNanos = startNanos;
75
+ if (rttNanos < this.windowMinRttNanos)
76
+ this.windowMinRttNanos = rttNanos;
77
+ if (inflight > this.windowMaxInflight)
78
+ this.windowMaxInflight = inflight;
79
+ if (didDrop)
80
+ this.windowDropped = true;
81
+ this.windowSamples++;
82
+ const elapsed = startNanos - this.windowStartNanos;
83
+ if (elapsed >= this.windowNanos && this.windowSamples >= this.minWindowSamples) {
84
+ this.commitWindow();
85
+ }
86
+ }
87
+ onChange(listener) {
88
+ return this.listeners.add(listener);
89
+ }
90
+ commitWindow() {
91
+ const current = this._limit;
92
+ const minRtt = this.windowMinRttNanos;
93
+ const maxInflight = this.windowMaxInflight;
94
+ const dropped = this.windowDropped;
95
+ // Reset window state up-front so we always start a clean window even if
96
+ // we early-return below.
97
+ this.windowStartNanos = -1;
98
+ this.windowMinRttNanos = Number.POSITIVE_INFINITY;
99
+ this.windowMaxInflight = 0;
100
+ this.windowSamples = 0;
101
+ this.windowDropped = false;
102
+ let next = current;
103
+ if (dropped) {
104
+ next = Math.max(this.minLimit, Math.floor(current * this.backoffRatio));
105
+ }
106
+ else {
107
+ // Track rtt-noload as a rolling minimum, re-probed periodically.
108
+ if (minRtt < this.rttNoLoadNanos) {
109
+ this.rttNoLoadNanos = minRtt;
110
+ this.windowsSinceNewMin = 0;
111
+ }
112
+ else if (++this.windowsSinceNewMin >= this.probeIntervalWindows) {
113
+ // Re-baseline so a transient one-off low sample doesn't pin us forever.
114
+ this.rttNoLoadNanos = minRtt;
115
+ this.windowsSinceNewMin = 0;
116
+ }
117
+ // Don't grow if we aren't using the limit we have.
118
+ if (maxInflight * 2 < current)
119
+ return;
120
+ const queue = current * (1 - this.rttNoLoadNanos / minRtt);
121
+ const alpha = this.alphaFn(current);
122
+ const beta = this.betaFn(current);
123
+ if (queue <= alpha) {
124
+ next = current + this.increaseFn(current);
125
+ }
126
+ else if (queue >= beta) {
127
+ next = current - this.decreaseFn(current);
128
+ }
129
+ }
130
+ next = Math.round(clamp(next, this.minLimit, this.maxLimit));
131
+ if (next !== current) {
132
+ this._limit = next;
133
+ this.listeners.emit(next);
134
+ }
135
+ }
136
+ }
137
+ //# sourceMappingURL=vegas-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vegas-limit.js","sourceRoot":"","sources":["../../src/limit/vegas-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA0CvD,MAAM,aAAa,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACvE,MAAM,YAAY,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACtE,MAAM,YAAY,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAElE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,UAAU;IACb,MAAM,CAAS;IACd,QAAQ,CAAS;IACjB,QAAQ,CAAS;IACjB,WAAW,CAAS;IACpB,gBAAgB,CAAS;IACzB,YAAY,CAAS;IACrB,oBAAoB,CAAS;IACrB,OAAO,CAA4B;IACnC,MAAM,CAA4B;IAClC,UAAU,CAA4B;IACtC,UAAU,CAA4B;IACtC,SAAS,GAAG,IAAI,eAAe,EAAE,CAAC;IAE3C,cAAc,GAAW,MAAM,CAAC,iBAAiB,CAAC;IAClD,gBAAgB,GAAG,CAAC,CAAC,CAAC;IACtB,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC7C,iBAAiB,GAAG,CAAC,CAAC;IACtB,aAAa,GAAG,CAAC,CAAC;IAClB,aAAa,GAAG,KAAK,CAAC;IACtB,kBAAkB,GAAG,CAAC,CAAC;IAE/B,YAAY,OAA0B,EAAE;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC;QAClC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,uBAAuB,CAAC,CAAC;QAC/D,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,CAAC,CAAC;QACxE,IAAI,CAAC,CAAC,OAAO,IAAI,GAAG,IAAI,OAAO,IAAI,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,UAAU,CAAC,4BAA4B,GAAG,KAAK,GAAG,GAAG,CAAC,CAAC;QACnE,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;QACpB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,aAAa,CAAC;QACrD,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAC;QACpD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,GAAG,CAAC;QAC7C,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC,oBAAoB,IAAI,EAAE,CAAC;QAC5D,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,aAAa,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,IAAI,YAAY,CAAC;QACxC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,YAAY,CAAC;QAChD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,YAAY,CAAC;IAClD,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,6EAA6E;IAC7E,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED,QAAQ,CAAC,UAAiB,EAAE,QAAe,EAAE,QAAgB,EAAE,OAAgB;QAC7E,IAAI,IAAI,CAAC,gBAAgB,GAAG,CAAC;YAAE,IAAI,CAAC,gBAAgB,GAAG,UAAU,CAAC;QAElE,IAAI,QAAQ,GAAG,IAAI,CAAC,iBAAiB;YAAE,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC;QACzE,IAAI,QAAQ,GAAG,IAAI,CAAC,iBAAiB;YAAE,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC;QACzE,IAAI,OAAO;YAAE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QACvC,IAAI,CAAC,aAAa,EAAE,CAAC;QAErB,MAAM,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC;QACnD,IAAI,OAAO,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC/E,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,QAA6B;QACpC,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAEO,YAAY;QAClB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,CAAC;QACtC,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,CAAC;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC;QAEnC,wEAAwE;QACxE,yBAAyB;QACzB,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,CAAC;QAClD,IAAI,CAAC,iBAAiB,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAE3B,IAAI,IAAI,GAAG,OAAO,CAAC;QAEnB,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;QAC1E,CAAC;aAAM,CAAC;YACN,iEAAiE;YACjE,IAAI,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;gBACjC,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC;gBAC7B,IAAI,CAAC,kBAAkB,GAAG,CAAC,CAAC;YAC9B,CAAC;iBAAM,IAAI,EAAE,IAAI,CAAC,kBAAkB,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;gBAClE,wEAAwE;gBACxE,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC;gBAC7B,IAAI,CAAC,kBAAkB,GAAG,CAAC,CAAC;YAC9B,CAAC;YAED,mDAAmD;YACnD,IAAI,WAAW,GAAG,CAAC,GAAG,OAAO;gBAAE,OAAO;YAEtC,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,CAAC;YAC3D,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACpC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAElC,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;gBACnB,IAAI,GAAG,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC5C,CAAC;iBAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBACzB,IAAI,GAAG,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;QAED,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC7D,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ export type { Limiter } from './limiter.ts';
2
+ export { SimpleLimiter, type SimpleLimiterOptions } from './simple-limiter.ts';
@@ -0,0 +1,2 @@
1
+ export { SimpleLimiter } from "./simple-limiter.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/limiter/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Listener } from '../types.ts';
2
+ /**
3
+ * Gate work against an adaptive concurrency ceiling. Acquire a permit before
4
+ * starting work; call exactly one of `onSuccess` / `onDropped` / `onIgnore` on
5
+ * the returned {@link Listener} when work completes.
6
+ */
7
+ export interface Limiter {
8
+ /**
9
+ * Attempt to acquire a permit. Returns `undefined` if the current in-flight
10
+ * count is at or above the algorithm's limit, in which case the caller is
11
+ * expected to shed load (e.g. respond 429, route to a fallback, queue).
12
+ */
13
+ acquire(): Listener | undefined;
14
+ /** Current in-flight count. */
15
+ readonly inflight: number;
16
+ /** Current algorithm limit. */
17
+ readonly limit: number;
18
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limiter.js","sourceRoot":"","sources":["../../src/limiter/limiter.ts"],"names":[],"mappings":""}
@@ -0,0 +1,28 @@
1
+ import { type Clock } from '../clock.ts';
2
+ import type { Limit } from '../limit/limit.ts';
3
+ import type { Listener, Nanos } from '../types.ts';
4
+ import type { Limiter } from './limiter.ts';
5
+ export interface SimpleLimiterOptions {
6
+ /** Time source. Defaults to {@link defaultClock} (monotonic, nanosecond). */
7
+ readonly clock?: Clock;
8
+ }
9
+ /**
10
+ * Minimal {@link Limiter}: tracks an in-flight counter and gates new permits
11
+ * against the underlying {@link Limit}'s ceiling. No queueing — over-limit
12
+ * calls return `undefined` immediately so the caller can shed load.
13
+ *
14
+ * Sample reporting on the hot path is allocation-free aside from the listener
15
+ * itself: each listener carries its own start state and calls a positional
16
+ * method on the limiter to release.
17
+ */
18
+ export declare class SimpleLimiter implements Limiter {
19
+ readonly algorithm: Limit;
20
+ readonly clock: Clock;
21
+ private _inflight;
22
+ constructor(algorithm: Limit, opts?: SimpleLimiterOptions);
23
+ get inflight(): number;
24
+ get limit(): number;
25
+ acquire(): Listener | undefined;
26
+ /** @internal */
27
+ _release(startNanos: Nanos, startInflight: number, didDrop: boolean, ignore: boolean): void;
28
+ }
@@ -0,0 +1,70 @@
1
+ import { defaultClock } from "../clock.js";
2
+ /**
3
+ * Minimal {@link Limiter}: tracks an in-flight counter and gates new permits
4
+ * against the underlying {@link Limit}'s ceiling. No queueing — over-limit
5
+ * calls return `undefined` immediately so the caller can shed load.
6
+ *
7
+ * Sample reporting on the hot path is allocation-free aside from the listener
8
+ * itself: each listener carries its own start state and calls a positional
9
+ * method on the limiter to release.
10
+ */
11
+ export class SimpleLimiter {
12
+ algorithm;
13
+ clock;
14
+ _inflight = 0;
15
+ constructor(algorithm, opts = {}) {
16
+ this.algorithm = algorithm;
17
+ this.clock = opts.clock ?? defaultClock;
18
+ }
19
+ get inflight() {
20
+ return this._inflight;
21
+ }
22
+ get limit() {
23
+ return this.algorithm.limit;
24
+ }
25
+ acquire() {
26
+ const next = this._inflight + 1;
27
+ if (next > this.algorithm.limit)
28
+ return undefined;
29
+ this._inflight = next;
30
+ return new SimpleListener(this, this.clock.nowNanos(), next);
31
+ }
32
+ /** @internal */
33
+ _release(startNanos, startInflight, didDrop, ignore) {
34
+ this._inflight--;
35
+ if (ignore)
36
+ return;
37
+ const rtt = this.clock.nowNanos() - startNanos;
38
+ this.algorithm.onSample(startNanos, rtt, startInflight, didDrop);
39
+ }
40
+ }
41
+ class SimpleListener {
42
+ limiter;
43
+ startNanos;
44
+ startInflight;
45
+ done = false;
46
+ constructor(limiter, startNanos, startInflight) {
47
+ this.limiter = limiter;
48
+ this.startNanos = startNanos;
49
+ this.startInflight = startInflight;
50
+ }
51
+ onSuccess() {
52
+ if (this.done)
53
+ return;
54
+ this.done = true;
55
+ this.limiter._release(this.startNanos, this.startInflight, false, false);
56
+ }
57
+ onDropped() {
58
+ if (this.done)
59
+ return;
60
+ this.done = true;
61
+ this.limiter._release(this.startNanos, this.startInflight, true, false);
62
+ }
63
+ onIgnore() {
64
+ if (this.done)
65
+ return;
66
+ this.done = true;
67
+ this.limiter._release(this.startNanos, this.startInflight, false, true);
68
+ }
69
+ }
70
+ //# sourceMappingURL=simple-limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple-limiter.js","sourceRoot":"","sources":["../../src/limiter/simple-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAc,MAAM,aAAa,CAAC;AAUvD;;;;;;;;GAQG;AACH,MAAM,OAAO,aAAa;IACf,SAAS,CAAQ;IACjB,KAAK,CAAQ;IACd,SAAS,GAAG,CAAC,CAAC;IAEtB,YAAY,SAAgB,EAAE,OAA6B,EAAE;QAC3D,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,YAAY,CAAC;IAC1C,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;IAC9B,CAAC;IAED,OAAO;QACL,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QAChC,IAAI,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAClD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,OAAO,IAAI,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,CAAC;IAC/D,CAAC;IAED,gBAAgB;IAChB,QAAQ,CAAC,UAAiB,EAAE,aAAqB,EAAE,OAAgB,EAAE,MAAe;QAClF,IAAI,CAAC,SAAS,EAAE,CAAC;QACjB,IAAI,MAAM;YAAE,OAAO;QACnB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,UAAU,CAAC;QAC/C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;CACF;AAED,MAAM,cAAc;IACD,OAAO,CAAgB;IACvB,UAAU,CAAQ;IAClB,aAAa,CAAS;IAC/B,IAAI,GAAG,KAAK,CAAC;IAErB,YAAY,OAAsB,EAAE,UAAiB,EAAE,aAAqB;QAC1E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACrC,CAAC;IAED,SAAS;QACP,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAC3E,CAAC;IAED,SAAS;QACP,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1E,CAAC;IAED,QAAQ;QACN,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAC1E,CAAC;CACF"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Time durations are represented as `number` nanoseconds carried in IEEE-754
3
+ * doubles. This keeps arithmetic on the hot path zero-allocation while still
4
+ * giving sub-microsecond precision for any realistic process uptime.
5
+ */
6
+ export type Nanos = number;
7
+ /**
8
+ * Returned by {@link Limiter.acquire} when a permit was successfully reserved.
9
+ * Exactly one of `onSuccess` / `onDropped` / `onIgnore` must be called for each
10
+ * permit; subsequent calls on the same listener are no-ops.
11
+ *
12
+ * Semantics:
13
+ * - `onSuccess` — work completed within expected bounds. RTT is informative.
14
+ * - `onDropped` — work failed in a way that indicates the upstream is
15
+ * overloaded (timeout, 5xx, queue-full). Strongest decrease
16
+ * signal for adaptive algorithms.
17
+ * - `onIgnore` — work completed but the sample should not influence the
18
+ * limit (e.g. client cancellation, 4xx validation error,
19
+ * cache hit). The permit is released; RTT is discarded.
20
+ */
21
+ export interface Listener {
22
+ onSuccess(): void;
23
+ onDropped(): void;
24
+ onIgnore(): void;
25
+ }
26
+ export type LimitChangeListener = (newLimit: number) => void;
27
+ export type Unsubscribe = () => void;
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Exponential moving average with lazy initialization. The first value seeds
3
+ * the average so the EMA does not bias toward zero during warm-up.
4
+ *
5
+ * Update formula (after init):
6
+ * v ← α·x + (1 − α)·v
7
+ *
8
+ * Hot path is one multiply + one fused-multiply-add equivalent; no allocations.
9
+ */
10
+ export declare class Ema {
11
+ readonly alpha: number;
12
+ private _value;
13
+ private _initialized;
14
+ /**
15
+ * @param alpha smoothing factor in (0, 1]. Higher α reacts faster, lower α
16
+ * smooths more. Equivalent time constant ≈ 1/α samples.
17
+ */
18
+ constructor(alpha: number);
19
+ /**
20
+ * Construct an EMA whose smoothing factor approximates `n` samples of memory.
21
+ */
22
+ static withWindow(n: number): Ema;
23
+ update(x: number): number;
24
+ get value(): number;
25
+ get initialized(): boolean;
26
+ reset(): void;
27
+ }