@prosopo/load-balancer 2.10.15 → 2.10.17

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.
@@ -1,30 +1,26 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const defaultSleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
4
- const getBackoffDelayMs = (attempt, baseDelayMs, maxDelayMs, random = Math.random) => {
5
- const safeAttempt = Math.max(0, Math.floor(attempt));
6
- const cap = Math.min(maxDelayMs, baseDelayMs * 2 ** safeAttempt);
7
- return Math.round(random() * cap);
1
+ //#region src/retry.ts
2
+ var defaultSleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
3
+ var getBackoffDelayMs = (attempt, baseDelayMs, maxDelayMs, random = Math.random) => {
4
+ const cap = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, Math.floor(attempt)));
5
+ return Math.round(random() * cap);
8
6
  };
9
- const retryWithBackoff = async (fn, opts) => {
10
- const {
11
- maxAttempts,
12
- baseDelayMs,
13
- maxDelayMs,
14
- random = Math.random,
15
- sleep = defaultSleep
16
- } = opts;
17
- let lastErr;
18
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
19
- try {
20
- return await fn();
21
- } catch (err) {
22
- lastErr = err;
23
- if (attempt >= maxAttempts - 1) break;
24
- await sleep(getBackoffDelayMs(attempt, baseDelayMs, maxDelayMs, random));
25
- }
26
- }
27
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
7
+ /**
8
+ * Run `fn` up to `maxAttempts` times, sleeping with full-jitter exponential
9
+ * backoff between attempts. Returns the first successful value. If every
10
+ * attempt throws, throws the last error (Error-normalised).
11
+ */
12
+ var retryWithBackoff = async (fn, opts) => {
13
+ const { maxAttempts, baseDelayMs, maxDelayMs, random = Math.random, sleep = defaultSleep } = opts;
14
+ let lastErr;
15
+ for (let attempt = 0; attempt < maxAttempts; attempt++) try {
16
+ return await fn();
17
+ } catch (err) {
18
+ lastErr = err;
19
+ if (attempt >= maxAttempts - 1) break;
20
+ await sleep(getBackoffDelayMs(attempt, baseDelayMs, maxDelayMs, random));
21
+ }
22
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
28
23
  };
24
+ //#endregion
29
25
  exports.getBackoffDelayMs = getBackoffDelayMs;
30
26
  exports.retryWithBackoff = retryWithBackoff;
package/dist/index.js CHANGED
@@ -1,16 +1,4 @@
1
- import { _resetHealthzRetryPolicy, _resetPinCache, _resetProviderListCache, _setHealthzRetryPolicy, getProviders, getRandomActiveProvider, getRandomProviderFromList } from "./providers.js";
1
+ import "./_virtual/_rolldown/runtime.js";
2
2
  import { convertHostedProvider, getLoadBalancerUrl, getProviderHostname, loadBalancer, stripIpModeLabel } from "./balancer.js";
3
- export {
4
- _resetHealthzRetryPolicy,
5
- _resetPinCache,
6
- _resetProviderListCache,
7
- _setHealthzRetryPolicy,
8
- convertHostedProvider,
9
- getLoadBalancerUrl,
10
- getProviderHostname,
11
- getProviders,
12
- getRandomActiveProvider,
13
- getRandomProviderFromList,
14
- loadBalancer,
15
- stripIpModeLabel
16
- };
3
+ import { _resetHealthzRetryPolicy, _resetPinCache, _resetProviderListCache, _setHealthzRetryPolicy, getProviders, getRandomActiveProvider, getRandomProviderFromList } from "./providers.js";
4
+ export { _resetHealthzRetryPolicy, _resetPinCache, _resetProviderListCache, _setHealthzRetryPolicy, convertHostedProvider, getLoadBalancerUrl, getProviderHostname, getProviders, getRandomActiveProvider, getRandomProviderFromList, loadBalancer, stripIpModeLabel };
package/dist/providers.js CHANGED
@@ -1,141 +1,138 @@
1
1
  import { loadBalancer } from "./balancer.js";
2
2
  import { retryWithBackoff } from "./retry.js";
3
- const DNS_ENDPOINT = {
4
- development: "https://localhost:9229",
5
- staging: "https://staging.pronode.prosopo.io",
6
- production: "https://pronode.prosopo.io"
3
+ //#region src/providers.ts
4
+ var DNS_ENDPOINT = {
5
+ development: "https://localhost:9229",
6
+ staging: "https://staging.pronode.prosopo.io",
7
+ production: "https://pronode.prosopo.io"
7
8
  };
8
- const withIpModeLabel = (hostname, ipMode) => ipMode ? `${ipMode}.${hostname}` : hostname;
9
- const applyIpModeToUrl = (url, ipMode) => {
10
- if (!ipMode) return url;
11
- try {
12
- const parsed = new URL(url);
13
- parsed.hostname = withIpModeLabel(parsed.hostname, ipMode);
14
- return parsed.toString().replace(/\/$/, "");
15
- } catch {
16
- return url;
17
- }
9
+ var withIpModeLabel = (hostname, ipMode) => ipMode ? `${ipMode}.${hostname}` : hostname;
10
+ var applyIpModeToUrl = (url, ipMode) => {
11
+ if (!ipMode) return url;
12
+ try {
13
+ const parsed = new URL(url);
14
+ parsed.hostname = withIpModeLabel(parsed.hostname, ipMode);
15
+ return parsed.toString().replace(/\/$/, "");
16
+ } catch {
17
+ return url;
18
+ }
18
19
  };
19
- const cacheKey = (env, ipMode) => `${env}|${ipMode ?? "dual"}`;
20
- const pinPromiseCache = /* @__PURE__ */ new Map();
21
- const HEALTHZ_MAX_ATTEMPTS = 3;
22
- const HEALTHZ_RETRY_BASE_DELAY_MS = 250;
23
- const HEALTHZ_RETRY_MAX_DELAY_MS = 2e3;
24
- let healthzMaxAttempts = HEALTHZ_MAX_ATTEMPTS;
25
- let healthzRetryBaseDelayMs = HEALTHZ_RETRY_BASE_DELAY_MS;
26
- let healthzRetryMaxDelayMs = HEALTHZ_RETRY_MAX_DELAY_MS;
27
- const fetchPinnedHost = async (baseUrl) => {
28
- const res = await fetch(`${baseUrl}/healthz`, {
29
- method: "GET",
30
- cache: "no-store",
31
- credentials: "omit"
32
- });
33
- if (!res.ok) {
34
- throw new Error(`healthz responded with ${res.status}`);
35
- }
36
- const body = await res.json();
37
- if (typeof body.host !== "string" || body.host.length === 0) {
38
- throw new Error("healthz response missing host field");
39
- }
40
- return body.host;
20
+ var cacheKey = (env, ipMode) => `${env}|${ipMode ?? "dual"}`;
21
+ var pinPromiseCache = /* @__PURE__ */ new Map();
22
+ var HEALTHZ_MAX_ATTEMPTS = 3;
23
+ var HEALTHZ_RETRY_BASE_DELAY_MS = 250;
24
+ var HEALTHZ_RETRY_MAX_DELAY_MS = 2e3;
25
+ var healthzMaxAttempts = HEALTHZ_MAX_ATTEMPTS;
26
+ var healthzRetryBaseDelayMs = HEALTHZ_RETRY_BASE_DELAY_MS;
27
+ var healthzRetryMaxDelayMs = HEALTHZ_RETRY_MAX_DELAY_MS;
28
+ var fetchPinnedHost = async (baseUrl) => {
29
+ const res = await fetch(`${baseUrl}/healthz`, {
30
+ method: "GET",
31
+ cache: "no-store",
32
+ credentials: "omit"
33
+ });
34
+ if (!res.ok) throw new Error(`healthz responded with ${res.status}`);
35
+ const body = await res.json();
36
+ if (typeof body.host !== "string" || body.host.length === 0) throw new Error("healthz response missing host field");
37
+ return body.host;
41
38
  };
42
- const fetchPinnedHostWithRetry = (baseUrl) => retryWithBackoff(() => fetchPinnedHost(baseUrl), {
43
- maxAttempts: healthzMaxAttempts,
44
- baseDelayMs: healthzRetryBaseDelayMs,
45
- maxDelayMs: healthzRetryMaxDelayMs
39
+ var fetchPinnedHostWithRetry = (baseUrl) => retryWithBackoff(() => fetchPinnedHost(baseUrl), {
40
+ maxAttempts: healthzMaxAttempts,
41
+ baseDelayMs: healthzRetryBaseDelayMs,
42
+ maxDelayMs: healthzRetryMaxDelayMs
46
43
  });
47
- const resolveBaseUrl = (env) => DNS_ENDPOINT[env] ?? DNS_ENDPOINT.development;
48
- const resolvePinnedUrl = async (env, ipMode) => {
49
- const base = applyIpModeToUrl(resolveBaseUrl(env), ipMode);
50
- if (env === "development") return base;
51
- const key = cacheKey(env, ipMode);
52
- const cached = pinPromiseCache.get(key);
53
- if (cached) return cached;
54
- const promise = (async () => {
55
- try {
56
- const host = await fetchPinnedHostWithRetry(base);
57
- const parsed = new URL(base);
58
- parsed.hostname = withIpModeLabel(host, ipMode);
59
- return parsed.toString().replace(/\/$/, "");
60
- } catch (err) {
61
- pinPromiseCache.delete(key);
62
- throw err;
63
- }
64
- })();
65
- pinPromiseCache.set(key, promise);
66
- return promise;
44
+ var resolveBaseUrl = (env) => DNS_ENDPOINT[env] ?? DNS_ENDPOINT.development;
45
+ var resolvePinnedUrl = async (env, ipMode) => {
46
+ const base = applyIpModeToUrl(resolveBaseUrl(env), ipMode);
47
+ if (env === "development") return base;
48
+ const key = cacheKey(env, ipMode);
49
+ const cached = pinPromiseCache.get(key);
50
+ if (cached) return cached;
51
+ const promise = (async () => {
52
+ try {
53
+ const host = await fetchPinnedHostWithRetry(base);
54
+ const parsed = new URL(base);
55
+ parsed.hostname = withIpModeLabel(host, ipMode);
56
+ return parsed.toString().replace(/\/$/, "");
57
+ } catch (err) {
58
+ pinPromiseCache.delete(key);
59
+ throw err;
60
+ }
61
+ })();
62
+ pinPromiseCache.set(key, promise);
63
+ return promise;
67
64
  };
68
- const providerListPromiseCache = /* @__PURE__ */ new Map();
69
- const getProviders = async (env) => {
70
- const cached = providerListPromiseCache.get(env);
71
- if (cached) return cached;
72
- const promise = loadBalancer(env).catch((err) => {
73
- providerListPromiseCache.delete(env);
74
- throw err;
75
- });
76
- providerListPromiseCache.set(env, promise);
77
- return promise;
65
+ var providerListPromiseCache = /* @__PURE__ */ new Map();
66
+ /**
67
+ * Returns the full (cached) list of active providers for an environment.
68
+ * Used to look up a provider by url/address, e.g. to find the provider that
69
+ * issued a token before forwarding a verification request to it.
70
+ */
71
+ var getProviders = async (env) => {
72
+ const cached = providerListPromiseCache.get(env);
73
+ if (cached) return cached;
74
+ const promise = loadBalancer(env).catch((err) => {
75
+ providerListPromiseCache.delete(env);
76
+ throw err;
77
+ });
78
+ providerListPromiseCache.set(env, promise);
79
+ return promise;
78
80
  };
79
- const getRandomActiveProvider = async (env, ipMode) => {
80
- const url = await resolvePinnedUrl(env, ipMode);
81
- return {
82
- providerAccount: "dns-routed",
83
- provider: { url }
84
- };
81
+ var getRandomActiveProvider = async (env, ipMode) => {
82
+ return {
83
+ providerAccount: "dns-routed",
84
+ provider: { url: await resolvePinnedUrl(env, ipMode) }
85
+ };
85
86
  };
86
- const sameProviderUrl = (a, b) => a.replace(/\/$/, "") === b.replace(/\/$/, "");
87
- const pickWeightedProvider = (providers, random) => {
88
- const totalWeight = providers.reduce((sum, p) => sum + p.weight, 0);
89
- let threshold = random() * totalWeight;
90
- let chosen;
91
- for (const provider of providers) {
92
- chosen = provider;
93
- threshold -= provider.weight;
94
- if (threshold < 0) break;
95
- }
96
- return chosen;
87
+ var sameProviderUrl = (a, b) => a.replace(/\/$/, "") === b.replace(/\/$/, "");
88
+ var pickWeightedProvider = (providers, random) => {
89
+ const totalWeight = providers.reduce((sum, p) => sum + p.weight, 0);
90
+ let threshold = random() * totalWeight;
91
+ let chosen;
92
+ for (const provider of providers) {
93
+ chosen = provider;
94
+ threshold -= provider.weight;
95
+ if (threshold < 0) break;
96
+ }
97
+ return chosen;
97
98
  };
98
- const getRandomProviderFromList = async (env, ipMode, excludeUrl, random = Math.random) => {
99
- const providers = await getProviders(env);
100
- if (providers.length === 0) {
101
- return getRandomActiveProvider(env, ipMode);
102
- }
103
- const eligible = excludeUrl && providers.length > 1 ? providers.filter(
104
- (p) => !sameProviderUrl(applyIpModeToUrl(p.url, ipMode), excludeUrl)
105
- ) : providers;
106
- const pool = eligible.length > 0 ? eligible : providers;
107
- const chosen = pickWeightedProvider(pool, random);
108
- if (!chosen) {
109
- return getRandomActiveProvider(env, ipMode);
110
- }
111
- return {
112
- providerAccount: chosen.address,
113
- provider: { url: applyIpModeToUrl(chosen.url, ipMode) }
114
- };
99
+ /**
100
+ * Pick a random provider directly from the provider list, bypassing the
101
+ * DNS-routed endpoint. This is the error-fallback path: once a provider has
102
+ * errored, retrying it re-hits the same (possibly-down) endpoint, and a fleet
103
+ * of widgets doing that in a tight loop can accidentally DDoS the provider — so
104
+ * instead we spread the retry across the fleet by choosing a random provider
105
+ * from the list. `excludeUrl` (the provider that just failed) is dropped from
106
+ * the pool when other providers remain. In development the list holds only the
107
+ * single local provider, so this naturally degrades to retrying that provider.
108
+ * `random` is injectable so tests can make the pick deterministic.
109
+ */
110
+ var getRandomProviderFromList = async (env, ipMode, excludeUrl, random = Math.random) => {
111
+ const providers = await getProviders(env);
112
+ if (providers.length === 0) return getRandomActiveProvider(env, ipMode);
113
+ const eligible = excludeUrl && providers.length > 1 ? providers.filter((p) => !sameProviderUrl(applyIpModeToUrl(p.url, ipMode), excludeUrl)) : providers;
114
+ const chosen = pickWeightedProvider(eligible.length > 0 ? eligible : providers, random);
115
+ if (!chosen) return getRandomActiveProvider(env, ipMode);
116
+ return {
117
+ providerAccount: chosen.address,
118
+ provider: { url: applyIpModeToUrl(chosen.url, ipMode) }
119
+ };
115
120
  };
116
- const _resetPinCache = () => {
117
- pinPromiseCache.clear();
121
+ var _resetPinCache = () => {
122
+ pinPromiseCache.clear();
118
123
  };
119
- const _resetProviderListCache = () => {
120
- providerListPromiseCache.clear();
124
+ var _resetProviderListCache = () => {
125
+ providerListPromiseCache.clear();
121
126
  };
122
- const _setHealthzRetryPolicy = (opts) => {
123
- if (opts.maxAttempts !== void 0) healthzMaxAttempts = opts.maxAttempts;
124
- if (opts.baseDelayMs !== void 0)
125
- healthzRetryBaseDelayMs = opts.baseDelayMs;
126
- if (opts.maxDelayMs !== void 0) healthzRetryMaxDelayMs = opts.maxDelayMs;
127
+ var _setHealthzRetryPolicy = (opts) => {
128
+ if (opts.maxAttempts !== void 0) healthzMaxAttempts = opts.maxAttempts;
129
+ if (opts.baseDelayMs !== void 0) healthzRetryBaseDelayMs = opts.baseDelayMs;
130
+ if (opts.maxDelayMs !== void 0) healthzRetryMaxDelayMs = opts.maxDelayMs;
127
131
  };
128
- const _resetHealthzRetryPolicy = () => {
129
- healthzMaxAttempts = HEALTHZ_MAX_ATTEMPTS;
130
- healthzRetryBaseDelayMs = HEALTHZ_RETRY_BASE_DELAY_MS;
131
- healthzRetryMaxDelayMs = HEALTHZ_RETRY_MAX_DELAY_MS;
132
- };
133
- export {
134
- _resetHealthzRetryPolicy,
135
- _resetPinCache,
136
- _resetProviderListCache,
137
- _setHealthzRetryPolicy,
138
- getProviders,
139
- getRandomActiveProvider,
140
- getRandomProviderFromList
132
+ var _resetHealthzRetryPolicy = () => {
133
+ healthzMaxAttempts = HEALTHZ_MAX_ATTEMPTS;
134
+ healthzRetryBaseDelayMs = HEALTHZ_RETRY_BASE_DELAY_MS;
135
+ healthzRetryMaxDelayMs = HEALTHZ_RETRY_MAX_DELAY_MS;
141
136
  };
137
+ //#endregion
138
+ export { _resetHealthzRetryPolicy, _resetPinCache, _resetProviderListCache, _setHealthzRetryPolicy, getProviders, getRandomActiveProvider, getRandomProviderFromList };
package/dist/retry.js CHANGED
@@ -1,30 +1,25 @@
1
- const defaultSleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
2
- const getBackoffDelayMs = (attempt, baseDelayMs, maxDelayMs, random = Math.random) => {
3
- const safeAttempt = Math.max(0, Math.floor(attempt));
4
- const cap = Math.min(maxDelayMs, baseDelayMs * 2 ** safeAttempt);
5
- return Math.round(random() * cap);
1
+ //#region src/retry.ts
2
+ var defaultSleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
3
+ var getBackoffDelayMs = (attempt, baseDelayMs, maxDelayMs, random = Math.random) => {
4
+ const cap = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, Math.floor(attempt)));
5
+ return Math.round(random() * cap);
6
6
  };
7
- const retryWithBackoff = async (fn, opts) => {
8
- const {
9
- maxAttempts,
10
- baseDelayMs,
11
- maxDelayMs,
12
- random = Math.random,
13
- sleep = defaultSleep
14
- } = opts;
15
- let lastErr;
16
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
17
- try {
18
- return await fn();
19
- } catch (err) {
20
- lastErr = err;
21
- if (attempt >= maxAttempts - 1) break;
22
- await sleep(getBackoffDelayMs(attempt, baseDelayMs, maxDelayMs, random));
23
- }
24
- }
25
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
26
- };
27
- export {
28
- getBackoffDelayMs,
29
- retryWithBackoff
7
+ /**
8
+ * Run `fn` up to `maxAttempts` times, sleeping with full-jitter exponential
9
+ * backoff between attempts. Returns the first successful value. If every
10
+ * attempt throws, throws the last error (Error-normalised).
11
+ */
12
+ var retryWithBackoff = async (fn, opts) => {
13
+ const { maxAttempts, baseDelayMs, maxDelayMs, random = Math.random, sleep = defaultSleep } = opts;
14
+ let lastErr;
15
+ for (let attempt = 0; attempt < maxAttempts; attempt++) try {
16
+ return await fn();
17
+ } catch (err) {
18
+ lastErr = err;
19
+ if (attempt >= maxAttempts - 1) break;
20
+ await sleep(getBackoffDelayMs(attempt, baseDelayMs, maxDelayMs, random));
21
+ }
22
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
30
23
  };
24
+ //#endregion
25
+ export { getBackoffDelayMs, retryWithBackoff };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosopo/load-balancer",
3
- "version": "2.10.15",
3
+ "version": "2.10.17",
4
4
  "description": "Provider load balancer",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,14 +37,14 @@
37
37
  },
38
38
  "homepage": "https://github.com/prosopo/captcha#readme",
39
39
  "dependencies": {
40
- "@prosopo/common": "3.1.47",
41
- "@prosopo/types": "4.9.12",
40
+ "@prosopo/common": "3.1.48",
41
+ "@prosopo/types": "5.0.0",
42
42
  "zod": "3.23.8"
43
43
  },
44
44
  "devDependencies": {
45
- "@prosopo/config": "3.3.4",
45
+ "@prosopo/config": "3.3.6",
46
46
  "@types/node": "22.10.2",
47
- "@vitest/coverage-v8": "3.2.4",
47
+ "@vitest/coverage-v8": "4.1.10",
48
48
  "concurrently": "9.0.1",
49
49
  "del-cli": "6.0.0",
50
50
  "dotenv": "17.2.1",
@@ -52,8 +52,8 @@
52
52
  "tslib": "2.7.0",
53
53
  "tsx": "4.20.3",
54
54
  "typescript": "5.6.2",
55
- "vite": "6.4.1",
56
- "vitest": "3.2.4"
55
+ "vite": "8.1.5",
56
+ "vitest": "4.1.10"
57
57
  },
58
58
  "sideEffects": false
59
59
  }