@uvrn/farm 3.0.0 → 4.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.
package/README.md CHANGED
@@ -71,6 +71,33 @@ const farm = new MultiFarm([
71
71
 
72
72
  `ConnectorRegistry` stores named connectors and can assemble a `MultiFarm` instance from all registered connectors or a named subset.
73
73
 
74
+ ## Rate limiting and circuit breaker
75
+
76
+ Every `BaseConnector` request made through `requestJson()` (or the protected `withGuards()` helper) is guarded by an in-process rate limiter and a circuit breaker. Both are configured per connector instance via `ConnectorConfig`:
77
+
78
+ | Option | Default | Behavior |
79
+ |---|---|---|
80
+ | `rateLimitPerMinute` | unset (no limit) | Sliding 60-second window. A request that would exceed the limit rejects with `RateLimitError` before any network I/O. |
81
+ | `circuitBreakerThreshold` | `5` | Consecutive request failures before the circuit opens. |
82
+ | `circuitBreakerResetMs` | `30_000` | Time the circuit stays open. After it elapses, one half-open probe is admitted — success closes the circuit, failure reopens it. |
83
+ | `now` | `Date.now` | Injectable clock (epoch ms) so tests can advance time without sleeping. |
84
+
85
+ While the circuit is open, requests reject fast with `CircuitOpenError`. Both error types extend `FarmConnectorError` and carry a `retryAfterMs` hint.
86
+
87
+ ```ts
88
+ import { CoinGeckoFarm, RateLimitError, CircuitOpenError } from '@uvrn/farm';
89
+
90
+ const connector = new CoinGeckoFarm({
91
+ rateLimitPerMinute: 30,
92
+ circuitBreakerThreshold: 5,
93
+ circuitBreakerResetMs: 30_000,
94
+ });
95
+ ```
96
+
97
+ **Zero-config note:** connectors constructed without these options behave exactly as before, except that 5 consecutive failures now open the breaker for 30 seconds (previously failures never tripped anything). There is no default rate limit.
98
+
99
+ Custom connectors that bypass `requestJson()` can opt in by wrapping their own I/O in `this.withGuards(fn)`.
100
+
74
101
  ## Public API
75
102
 
76
103
  - `BaseConnector`
@@ -88,6 +115,8 @@ const farm = new MultiFarm([
88
115
  - `ConnectorConfig`
89
116
  - `MultiFarmOptions`
90
117
  - `FarmConnectorError`
118
+ - `RateLimitError`
119
+ - `CircuitOpenError`
91
120
 
92
121
  ## Dependencies
93
122
 
@@ -2,13 +2,52 @@ import type { ClaimRegistration, FarmConnector, FarmResult } from '@uvrn/agent';
2
2
  import { type ConnectorConfig } from '../../types';
3
3
  export declare abstract class BaseConnector implements FarmConnector {
4
4
  abstract readonly name: string;
5
- protected readonly config: Required<Pick<ConnectorConfig, 'timeout' | 'maxRetries'>> & ConnectorConfig;
5
+ protected readonly config: Required<Pick<ConnectorConfig, 'timeout' | 'maxRetries' | 'circuitBreakerThreshold' | 'circuitBreakerResetMs'>> & ConnectorConfig;
6
+ /** Injectable clock (epoch ms) — defaults to `Date.now`. */
7
+ private readonly now;
8
+ /** Timestamps of requests made inside the current sliding 60s window. */
9
+ private requestTimestamps;
10
+ /** Consecutive request failures while the circuit is closed. */
11
+ private consecutiveFailures;
12
+ /** Epoch ms at which the circuit opened, or `null` when closed. */
13
+ private circuitOpenedAt;
14
+ /** True while the single half-open probe request is in flight. */
15
+ private halfOpenProbeInFlight;
6
16
  constructor(config?: ConnectorConfig);
7
17
  abstract fetch(claim: ClaimRegistration): Promise<FarmResult>;
8
18
  protected claimFromString(claim: string): ClaimRegistration;
9
19
  protected withRetry<T>(fn: () => Promise<T>): Promise<T>;
10
20
  protected withTimeout<T>(fn: () => Promise<T>, ms?: number): Promise<T>;
11
21
  protected requestJson<T>(url: string, init?: RequestInit): Promise<T>;
22
+ /**
23
+ * Runs `fn` behind the connector's circuit breaker and rate limiter.
24
+ *
25
+ * Rejects with {@link CircuitOpenError} (without invoking `fn`) while the
26
+ * circuit is open, and with {@link RateLimitError} when the call would
27
+ * exceed `rateLimitPerMinute` within the sliding 60-second window. Each
28
+ * guarded call consumes one rate-limit slot. A guarded failure counts
29
+ * toward `circuitBreakerThreshold` (default 5 consecutive failures open
30
+ * the circuit); after `circuitBreakerResetMs` (default 30 000 ms) the
31
+ * circuit goes half-open and admits a single probe — success closes the
32
+ * circuit, failure reopens it.
33
+ *
34
+ * `requestJson()` is wrapped automatically; custom connectors that bypass
35
+ * `requestJson()` can wrap their own I/O with this helper.
36
+ */
37
+ protected withGuards<T>(fn: () => Promise<T>): Promise<T>;
38
+ /**
39
+ * Validates circuit state before a request. Throws {@link CircuitOpenError}
40
+ * while open; returns `true` when this call is the half-open probe.
41
+ */
42
+ private checkCircuit;
43
+ /**
44
+ * Throws {@link RateLimitError} when another request would exceed the
45
+ * configured `rateLimitPerMinute`; otherwise records the request timestamp.
46
+ * No-op when `rateLimitPerMinute` is not configured.
47
+ */
48
+ private enforceRateLimit;
49
+ private recordSuccess;
50
+ private recordFailure;
12
51
  protected buildResult(claim: ClaimRegistration, sources: FarmResult['sources'], startedAt: number): FarmResult;
13
52
  protected requireApiKey(): string;
14
53
  private sleep;
@@ -1 +1 @@
1
- {"version":3,"file":"BaseConnector.d.ts","sourceRoot":"","sources":["../../../src/connectors/base/BaseConnector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEhF,OAAO,EAAE,KAAK,eAAe,EAAsB,MAAM,aAAa,CAAC;AAGvE,8BAAsB,aAAc,YAAW,aAAa;IAC1D,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,YAAY,CAAC,CAAC,GAAG,eAAe,CAAC;gBAE3F,MAAM,GAAE,eAAoB;IAQxC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC;IAE7D,SAAS,CAAC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB;cAI3C,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;cAkB9C,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;cAoB7D,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IAc3E,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU;IAS9G,SAAS,CAAC,aAAa,IAAI,MAAM;IAQjC,OAAO,CAAC,KAAK;CAGd"}
1
+ {"version":3,"file":"BaseConnector.d.ts","sourceRoot":"","sources":["../../../src/connectors/base/BaseConnector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEhF,OAAO,EAAE,KAAK,eAAe,EAAwD,MAAM,aAAa,CAAC;AAKzG,8BAAsB,aAAc,YAAW,aAAa;IAC1D,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CACjC,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,YAAY,GAAG,yBAAyB,GAAG,uBAAuB,CAAC,CACtG,GAAG,eAAe,CAAC;IAEpB,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,yEAAyE;IACzE,OAAO,CAAC,iBAAiB,CAAgB;IACzC,gEAAgE;IAChE,OAAO,CAAC,mBAAmB,CAAK;IAChC,mEAAmE;IACnE,OAAO,CAAC,eAAe,CAAuB;IAC9C,kEAAkE;IAClE,OAAO,CAAC,qBAAqB,CAAS;gBAE1B,MAAM,GAAE,eAAoB;IAWxC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC;IAE7D,SAAS,CAAC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB;cAI3C,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;cAkB9C,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;cAoB7D,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IAgB3E;;;;;;;;;;;;;;OAcG;cACa,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAuB/D;;;OAGG;IACH,OAAO,CAAC,YAAY;IAoBpB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,aAAa;IAerB,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU;IAS9G,SAAS,CAAC,aAAa,IAAI,MAAM;IAQjC,OAAO,CAAC,KAAK;CAGd"}
@@ -3,14 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseConnector = void 0;
4
4
  const types_1 = require("../../types");
5
5
  const defaultClaimRegistration_1 = require("../../internal/defaultClaimRegistration");
6
+ const RATE_LIMIT_WINDOW_MS = 60_000;
6
7
  class BaseConnector {
7
8
  config;
9
+ /** Injectable clock (epoch ms) — defaults to `Date.now`. */
10
+ now;
11
+ /** Timestamps of requests made inside the current sliding 60s window. */
12
+ requestTimestamps = [];
13
+ /** Consecutive request failures while the circuit is closed. */
14
+ consecutiveFailures = 0;
15
+ /** Epoch ms at which the circuit opened, or `null` when closed. */
16
+ circuitOpenedAt = null;
17
+ /** True while the single half-open probe request is in flight. */
18
+ halfOpenProbeInFlight = false;
8
19
  constructor(config = {}) {
9
20
  this.config = {
10
21
  timeout: 10_000,
11
22
  maxRetries: 3,
23
+ circuitBreakerThreshold: 5,
24
+ circuitBreakerResetMs: 30_000,
12
25
  ...config,
13
26
  };
27
+ this.now = config.now ?? Date.now;
14
28
  }
15
29
  claimFromString(claim) {
16
30
  return (0, defaultClaimRegistration_1.defaultClaimRegistration)(claim);
@@ -49,13 +63,105 @@ class BaseConnector {
49
63
  });
50
64
  }
51
65
  async requestJson(url, init) {
52
- return this.withRetry(async () => this.withTimeout(async () => {
66
+ return this.withGuards(async () => this.withRetry(async () => this.withTimeout(async () => {
53
67
  const response = await fetch(url, init);
54
68
  if (!response.ok) {
55
69
  throw new types_1.FarmConnectorError(this.name, `Request failed with status ${response.status}`);
56
70
  }
57
71
  return response.json();
58
- }));
72
+ })));
73
+ }
74
+ /**
75
+ * Runs `fn` behind the connector's circuit breaker and rate limiter.
76
+ *
77
+ * Rejects with {@link CircuitOpenError} (without invoking `fn`) while the
78
+ * circuit is open, and with {@link RateLimitError} when the call would
79
+ * exceed `rateLimitPerMinute` within the sliding 60-second window. Each
80
+ * guarded call consumes one rate-limit slot. A guarded failure counts
81
+ * toward `circuitBreakerThreshold` (default 5 consecutive failures open
82
+ * the circuit); after `circuitBreakerResetMs` (default 30 000 ms) the
83
+ * circuit goes half-open and admits a single probe — success closes the
84
+ * circuit, failure reopens it.
85
+ *
86
+ * `requestJson()` is wrapped automatically; custom connectors that bypass
87
+ * `requestJson()` can wrap their own I/O with this helper.
88
+ */
89
+ async withGuards(fn) {
90
+ const isProbe = this.checkCircuit();
91
+ try {
92
+ this.enforceRateLimit();
93
+ const value = await fn();
94
+ this.recordSuccess();
95
+ return value;
96
+ }
97
+ catch (error) {
98
+ if (error instanceof types_1.RateLimitError) {
99
+ // A rate-limited call never reached the network; release the probe
100
+ // slot (if held) without counting a breaker failure.
101
+ if (isProbe) {
102
+ this.halfOpenProbeInFlight = false;
103
+ }
104
+ throw error;
105
+ }
106
+ this.recordFailure(isProbe);
107
+ throw error;
108
+ }
109
+ }
110
+ /**
111
+ * Validates circuit state before a request. Throws {@link CircuitOpenError}
112
+ * while open; returns `true` when this call is the half-open probe.
113
+ */
114
+ checkCircuit() {
115
+ if (this.circuitOpenedAt == null) {
116
+ return false;
117
+ }
118
+ const elapsed = this.now() - this.circuitOpenedAt;
119
+ const remaining = this.config.circuitBreakerResetMs - elapsed;
120
+ if (remaining > 0) {
121
+ throw new types_1.CircuitOpenError(this.name, remaining);
122
+ }
123
+ if (this.halfOpenProbeInFlight) {
124
+ throw new types_1.CircuitOpenError(this.name, this.config.circuitBreakerResetMs);
125
+ }
126
+ this.halfOpenProbeInFlight = true;
127
+ return true;
128
+ }
129
+ /**
130
+ * Throws {@link RateLimitError} when another request would exceed the
131
+ * configured `rateLimitPerMinute`; otherwise records the request timestamp.
132
+ * No-op when `rateLimitPerMinute` is not configured.
133
+ */
134
+ enforceRateLimit() {
135
+ const limit = this.config.rateLimitPerMinute;
136
+ if (limit == null) {
137
+ return;
138
+ }
139
+ const now = this.now();
140
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
141
+ this.requestTimestamps = this.requestTimestamps.filter((timestamp) => timestamp > windowStart);
142
+ if (this.requestTimestamps.length >= limit) {
143
+ const oldest = this.requestTimestamps[0] ?? now;
144
+ throw new types_1.RateLimitError(this.name, limit, oldest + RATE_LIMIT_WINDOW_MS - now);
145
+ }
146
+ this.requestTimestamps.push(now);
147
+ }
148
+ recordSuccess() {
149
+ this.consecutiveFailures = 0;
150
+ this.circuitOpenedAt = null;
151
+ this.halfOpenProbeInFlight = false;
152
+ }
153
+ recordFailure(wasProbe) {
154
+ if (wasProbe) {
155
+ // Failed half-open probe: reopen for a full reset period.
156
+ this.halfOpenProbeInFlight = false;
157
+ this.circuitOpenedAt = this.now();
158
+ return;
159
+ }
160
+ this.consecutiveFailures += 1;
161
+ if (this.consecutiveFailures >= this.config.circuitBreakerThreshold) {
162
+ this.circuitOpenedAt = this.now();
163
+ this.consecutiveFailures = 0;
164
+ }
59
165
  }
60
166
  buildResult(claim, sources, startedAt) {
61
167
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"BaseConnector.js","sourceRoot":"","sources":["../../../src/connectors/base/BaseConnector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BaseConnector.js","sourceRoot":"","sources":["../../../src/connectors/base/BaseConnector.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,28 @@ export interface ConnectorConfig {
3
3
  apiKey?: string;
4
4
  timeout?: number;
5
5
  maxRetries?: number;
6
+ /**
7
+ * Maximum number of outbound requests allowed per sliding 60-second window.
8
+ * When unset, no rate limit is enforced (default behavior).
9
+ */
6
10
  rateLimitPerMinute?: number;
11
+ /**
12
+ * Number of consecutive request failures before the circuit breaker opens.
13
+ * While open, requests reject immediately with {@link CircuitOpenError}.
14
+ * Defaults to 5.
15
+ */
16
+ circuitBreakerThreshold?: number;
17
+ /**
18
+ * Milliseconds the circuit stays open before transitioning to half-open,
19
+ * at which point a single probe request is allowed through. Defaults to 30_000.
20
+ */
21
+ circuitBreakerResetMs?: number;
22
+ /**
23
+ * Injectable clock returning the current epoch milliseconds. Used by the
24
+ * rate limiter and circuit breaker; defaults to `Date.now`. Override in
25
+ * tests to advance time without sleeping.
26
+ */
27
+ now?: () => number;
7
28
  }
8
29
  export interface MultiFarmOptions {
9
30
  timeoutMs?: number;
@@ -14,4 +35,24 @@ export declare class FarmConnectorError extends Error {
14
35
  readonly cause?: unknown;
15
36
  constructor(connectorName: string, message: string, cause?: unknown);
16
37
  }
38
+ /**
39
+ * Thrown when a request would exceed the connector's configured
40
+ * `rateLimitPerMinute`. The request is rejected before any network I/O.
41
+ */
42
+ export declare class RateLimitError extends FarmConnectorError {
43
+ /** The configured per-minute limit that was hit. */
44
+ readonly limit: number;
45
+ /** Milliseconds until the oldest in-window request expires and a slot frees up. */
46
+ readonly retryAfterMs: number;
47
+ constructor(connectorName: string, limit: number, retryAfterMs: number);
48
+ }
49
+ /**
50
+ * Thrown when the connector's circuit breaker is open (or a half-open probe
51
+ * is already in flight). The request is rejected before any network I/O.
52
+ */
53
+ export declare class CircuitOpenError extends FarmConnectorError {
54
+ /** Milliseconds until the circuit transitions to half-open and allows a probe. */
55
+ readonly retryAfterMs: number;
56
+ constructor(connectorName: string, retryAfterMs: number);
57
+ }
17
58
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,SAAkB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEtB,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAMpE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,SAAkB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEtB,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAMpE;AAED;;;GAGG;AACH,qBAAa,cAAe,SAAQ,kBAAkB;IACpD,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAMvE;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,kBAAkB;IACtD,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAKxD"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FarmConnectorError = void 0;
3
+ exports.CircuitOpenError = exports.RateLimitError = exports.FarmConnectorError = void 0;
4
4
  class FarmConnectorError extends Error {
5
5
  connectorName;
6
6
  cause;
@@ -12,4 +12,35 @@ class FarmConnectorError extends Error {
12
12
  }
13
13
  }
14
14
  exports.FarmConnectorError = FarmConnectorError;
15
+ /**
16
+ * Thrown when a request would exceed the connector's configured
17
+ * `rateLimitPerMinute`. The request is rejected before any network I/O.
18
+ */
19
+ class RateLimitError extends FarmConnectorError {
20
+ /** The configured per-minute limit that was hit. */
21
+ limit;
22
+ /** Milliseconds until the oldest in-window request expires and a slot frees up. */
23
+ retryAfterMs;
24
+ constructor(connectorName, limit, retryAfterMs) {
25
+ super(connectorName, `Rate limit of ${limit} requests/minute exceeded; retry in ${retryAfterMs}ms`);
26
+ this.name = 'RateLimitError';
27
+ this.limit = limit;
28
+ this.retryAfterMs = retryAfterMs;
29
+ }
30
+ }
31
+ exports.RateLimitError = RateLimitError;
32
+ /**
33
+ * Thrown when the connector's circuit breaker is open (or a half-open probe
34
+ * is already in flight). The request is rejected before any network I/O.
35
+ */
36
+ class CircuitOpenError extends FarmConnectorError {
37
+ /** Milliseconds until the circuit transitions to half-open and allows a probe. */
38
+ retryAfterMs;
39
+ constructor(connectorName, retryAfterMs) {
40
+ super(connectorName, `Circuit breaker is open; retry in ${retryAfterMs}ms`);
41
+ this.name = 'CircuitOpenError';
42
+ this.retryAfterMs = retryAfterMs;
43
+ }
44
+ }
45
+ exports.CircuitOpenError = CircuitOpenError;
15
46
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;AAmBA,MAAa,kBAAmB,SAAQ,KAAK;IAClC,aAAa,CAAS;IACb,KAAK,CAAW;IAElC,YAAY,aAAqB,EAAE,OAAe,EAAE,KAAe;QACjE,KAAK,CAAC,IAAI,aAAa,KAAK,OAAO,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAVD,gDAUC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;AAwCA,MAAa,kBAAmB,SAAQ,KAAK;IAClC,aAAa,CAAS;IACb,KAAK,CAAW;IAElC,YAAY,aAAqB,EAAE,OAAe,EAAE,KAAe;QACjE,KAAK,CAAC,IAAI,aAAa,KAAK,OAAO,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAVD,gDAUC;AAED;;;GAGG;AACH,MAAa,cAAe,SAAQ,kBAAkB;IACpD,oDAAoD;IAC3C,KAAK,CAAS;IACvB,mFAAmF;IAC1E,YAAY,CAAS;IAE9B,YAAY,aAAqB,EAAE,KAAa,EAAE,YAAoB;QACpE,KAAK,CAAC,aAAa,EAAE,iBAAiB,KAAK,uCAAuC,YAAY,IAAI,CAAC,CAAC;QACpG,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;CACF;AAZD,wCAYC;AAED;;;GAGG;AACH,MAAa,gBAAiB,SAAQ,kBAAkB;IACtD,kFAAkF;IACzE,YAAY,CAAS;IAE9B,YAAY,aAAqB,EAAE,YAAoB;QACrD,KAAK,CAAC,aAAa,EAAE,qCAAqC,YAAY,IAAI,CAAC,CAAC;QAC5E,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;CACF;AATD,4CASC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uvrn/farm",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Standardized data source connectors for the UVRN protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,19 +19,26 @@
19
19
  "default": "./dist/connectors/index.js"
20
20
  }
21
21
  },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "test": "jest",
25
+ "test:coverage": "jest --coverage",
26
+ "test:watch": "jest --watch",
27
+ "clean": "rm -rf dist"
28
+ },
22
29
  "peerDependencies": {
23
- "@uvrn/core": "^3.0.0",
24
- "@uvrn/agent": "^3.0.0"
30
+ "@uvrn/core": "^4.0.0",
31
+ "@uvrn/agent": "^4.0.0"
25
32
  },
26
33
  "devDependencies": {
34
+ "@uvrn/core": "workspace:*",
35
+ "@uvrn/agent": "workspace:*",
36
+ "@uvrn/test": "workspace:*",
27
37
  "@types/jest": "^29.5.0",
28
38
  "@types/node": "^20.0.0",
29
39
  "jest": "^29.5.0",
30
40
  "ts-jest": "^29.1.0",
31
- "typescript": "^5.3.0",
32
- "@uvrn/core": "3.0.0",
33
- "@uvrn/agent": "3.0.0",
34
- "@uvrn/test": "3.0.0"
41
+ "typescript": "^5.3.0"
35
42
  },
36
43
  "engines": {
37
44
  "node": ">=18.0.0"
@@ -52,11 +59,5 @@
52
59
  },
53
60
  "publishConfig": {
54
61
  "access": "public"
55
- },
56
- "scripts": {
57
- "build": "tsc",
58
- "test": "jest",
59
- "test:watch": "jest --watch",
60
- "clean": "rm -rf dist"
61
62
  }
62
- }
63
+ }