@prosopo/load-balancer 2.10.15 → 2.10.16
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/.turbo/turbo-build$colon$cjs.log +12 -10
- package/.turbo/turbo-build$colon$tsc.log +11 -11
- package/.turbo/turbo-build.log +14 -11
- package/CHANGELOG.md +10 -0
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/balancer.js +32 -52
- package/dist/cjs/balancer.cjs +33 -49
- package/dist/cjs/index.cjs +14 -15
- package/dist/cjs/providers.cjs +124 -121
- package/dist/cjs/retry.cjs +22 -26
- package/dist/index.js +3 -15
- package/dist/providers.js +123 -126
- package/dist/retry.js +23 -28
- package/package.json +7 -7
- package/tsconfig.tsbuildinfo +1 -1
package/dist/cjs/retry.cjs
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
1
|
+
import "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { convertHostedProvider, getLoadBalancerUrl, getProviderHostname, loadBalancer, stripIpModeLabel } from "./balancer.js";
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
var fetchPinnedHostWithRetry = (baseUrl) => retryWithBackoff(() => fetchPinnedHost(baseUrl), {
|
|
40
|
+
maxAttempts: healthzMaxAttempts,
|
|
41
|
+
baseDelayMs: healthzRetryBaseDelayMs,
|
|
42
|
+
maxDelayMs: healthzRetryMaxDelayMs
|
|
46
43
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
81
|
+
var getRandomActiveProvider = async (env, ipMode) => {
|
|
82
|
+
return {
|
|
83
|
+
providerAccount: "dns-routed",
|
|
84
|
+
provider: { url: await resolvePinnedUrl(env, ipMode) }
|
|
85
|
+
};
|
|
85
86
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
121
|
+
var _resetPinCache = () => {
|
|
122
|
+
pinPromiseCache.clear();
|
|
118
123
|
};
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
var _resetProviderListCache = () => {
|
|
125
|
+
providerListPromiseCache.clear();
|
|
121
126
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
3
|
+
"version": "2.10.16",
|
|
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.
|
|
41
|
-
"@prosopo/types": "4.
|
|
40
|
+
"@prosopo/common": "3.1.48",
|
|
41
|
+
"@prosopo/types": "4.10.0",
|
|
42
42
|
"zod": "3.23.8"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@prosopo/config": "3.3.
|
|
45
|
+
"@prosopo/config": "3.3.5",
|
|
46
46
|
"@types/node": "22.10.2",
|
|
47
|
-
"@vitest/coverage-v8": "
|
|
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": "
|
|
56
|
-
"vitest": "
|
|
55
|
+
"vite": "8.1.5",
|
|
56
|
+
"vitest": "4.1.10"
|
|
57
57
|
},
|
|
58
58
|
"sideEffects": false
|
|
59
59
|
}
|