@prosopo/load-balancer 2.9.21 → 2.10.1
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 +5 -5
- package/.turbo/turbo-build$colon$tsc.log +11 -11
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +82 -0
- package/dist/balancer.d.ts +4 -1
- package/dist/balancer.d.ts.map +1 -1
- package/dist/balancer.js +10 -5
- package/dist/balancer.js.map +1 -1
- package/dist/cjs/balancer.cjs +9 -4
- package/dist/cjs/index.cjs +5 -4
- package/dist/cjs/providers.cjs +69 -51
- package/dist/index.js +7 -6
- package/dist/providers.d.ts +5 -6
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +70 -52
- package/dist/providers.js.map +1 -1
- package/dist/tests/providers.unit.test.js +112 -204
- package/dist/tests/providers.unit.test.js.map +1 -1
- package/package.json +4 -4
- package/src/balancer.ts +29 -3
- package/src/providers.ts +124 -90
- package/src/tests/providers.unit.test.ts +131 -262
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,224 +1,132 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
loadBalancer: vi.fn(),
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { _resetPinCache, _resetProviderListCache, getProviders, getRandomActiveProvider, } from "../providers.js";
|
|
3
|
+
const loadBalancer = vi.fn();
|
|
4
|
+
vi.mock("../balancer.js", () => ({
|
|
5
|
+
loadBalancer: (...args) => loadBalancer(...args),
|
|
7
6
|
}));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
expect(selectWeightedProvider(providers, 0).address).toBe("address1");
|
|
31
|
-
expect(selectWeightedProvider(providers, 1).address).toBe("address2");
|
|
32
|
-
expect(selectWeightedProvider(providers, 2).address).toBe("address2");
|
|
33
|
-
expect(selectWeightedProvider(providers, 3).address).toBe("address2");
|
|
34
|
-
expect(selectWeightedProvider(providers, 4).address).toBe("address3");
|
|
35
|
-
expect(selectWeightedProvider(providers, 5).address).toBe("address1");
|
|
36
|
-
expect(selectWeightedProvider(providers, 6).address).toBe("address2");
|
|
7
|
+
const originalFetch = globalThis.fetch;
|
|
8
|
+
const mockHealthzFetch = (host, ok = true, status = 200) => {
|
|
9
|
+
const mocked = vi.fn(async () => ({
|
|
10
|
+
ok,
|
|
11
|
+
status,
|
|
12
|
+
json: async () => ({ ok: true, host }),
|
|
13
|
+
}));
|
|
14
|
+
globalThis.fetch = mocked;
|
|
15
|
+
return mocked;
|
|
16
|
+
};
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
_resetPinCache();
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
globalThis.fetch = originalFetch;
|
|
22
|
+
});
|
|
23
|
+
describe("getRandomActiveProvider (dual stack)", () => {
|
|
24
|
+
it("uses the local URL directly in development (no healthz round-trip)", async () => {
|
|
25
|
+
const mocked = mockHealthzFetch("ignored");
|
|
26
|
+
const result = await getRandomActiveProvider("development");
|
|
27
|
+
expect(result.provider.url).toBe("https://localhost:9229");
|
|
28
|
+
expect(mocked).not.toHaveBeenCalled();
|
|
37
29
|
});
|
|
38
|
-
it("
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
url: "url1",
|
|
43
|
-
datasetId: "dataset1",
|
|
44
|
-
weight: 1,
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
address: "address2",
|
|
48
|
-
url: "url2",
|
|
49
|
-
datasetId: "dataset2",
|
|
50
|
-
weight: 1,
|
|
51
|
-
},
|
|
52
|
-
];
|
|
53
|
-
expect(selectWeightedProvider(providers, 0).address).toBe("address1");
|
|
54
|
-
expect(selectWeightedProvider(providers, 1).address).toBe("address2");
|
|
55
|
-
expect(selectWeightedProvider(providers, 2).address).toBe("address1");
|
|
56
|
-
expect(selectWeightedProvider(providers, 3).address).toBe("address2");
|
|
30
|
+
it("pins to the host returned by /healthz in production", async () => {
|
|
31
|
+
mockHealthzFetch("pronode7.prosopo.io");
|
|
32
|
+
const result = await getRandomActiveProvider("production");
|
|
33
|
+
expect(result.provider.url).toBe("https://pronode7.prosopo.io");
|
|
57
34
|
});
|
|
58
|
-
it("
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
url: "url1",
|
|
63
|
-
datasetId: "dataset1",
|
|
64
|
-
weight: 10,
|
|
65
|
-
},
|
|
66
|
-
];
|
|
67
|
-
expect(selectWeightedProvider(providers, 0).address).toBe("address1");
|
|
68
|
-
expect(selectWeightedProvider(providers, 5).address).toBe("address1");
|
|
69
|
-
expect(selectWeightedProvider(providers, 100).address).toBe("address1");
|
|
35
|
+
it("pins to the host returned by /healthz in staging", async () => {
|
|
36
|
+
mockHealthzFetch("staging-pronode2.prosopo.io");
|
|
37
|
+
const result = await getRandomActiveProvider("staging");
|
|
38
|
+
expect(result.provider.url).toBe("https://staging-pronode2.prosopo.io");
|
|
70
39
|
});
|
|
71
|
-
it("
|
|
72
|
-
|
|
40
|
+
it("caches the pinned URL for repeat callers in the same env + ipMode", async () => {
|
|
41
|
+
const mocked = mockHealthzFetch("pronode3.prosopo.io");
|
|
42
|
+
await getRandomActiveProvider("production");
|
|
43
|
+
await getRandomActiveProvider("production");
|
|
44
|
+
await getRandomActiveProvider("production");
|
|
45
|
+
expect(mocked).toHaveBeenCalledTimes(1);
|
|
73
46
|
});
|
|
74
|
-
it("
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
url: "url1",
|
|
79
|
-
datasetId: "dataset1",
|
|
80
|
-
weight: 1,
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
address: "address2",
|
|
84
|
-
url: "url2",
|
|
85
|
-
datasetId: "dataset2",
|
|
86
|
-
weight: 99,
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
|
-
const selections = { address1: 0, address2: 0 };
|
|
90
|
-
for (let i = 0; i < 100; i++) {
|
|
91
|
-
const selected = selectWeightedProvider(providers, i);
|
|
92
|
-
if (selected.address === "address1") {
|
|
93
|
-
selections.address1++;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
selections.address2++;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
expect(selections.address1).toBe(1);
|
|
100
|
-
expect(selections.address2).toBe(99);
|
|
47
|
+
it("falls back to the dual-stack base URL when /healthz responds non-OK", async () => {
|
|
48
|
+
mockHealthzFetch("ignored", false, 503);
|
|
49
|
+
const result = await getRandomActiveProvider("production");
|
|
50
|
+
expect(result.provider.url).toBe("https://pronode.prosopo.io");
|
|
101
51
|
});
|
|
102
|
-
it("
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
address: "address2",
|
|
112
|
-
url: "url2",
|
|
113
|
-
datasetId: "dataset2",
|
|
114
|
-
weight: 100,
|
|
115
|
-
},
|
|
116
|
-
];
|
|
117
|
-
const selections = { address1: 0, address2: 0 };
|
|
118
|
-
for (let i = 0; i < 200; i++) {
|
|
119
|
-
const selected = selectWeightedProvider(providers, i);
|
|
120
|
-
if (selected.address === "address1") {
|
|
121
|
-
selections.address1++;
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
selections.address2++;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
expect(selections.address1).toBe(100);
|
|
128
|
-
expect(selections.address2).toBe(100);
|
|
52
|
+
it("falls back to the dual-stack base URL when /healthz body is malformed", async () => {
|
|
53
|
+
const mocked = vi.fn(async () => ({
|
|
54
|
+
ok: true,
|
|
55
|
+
status: 200,
|
|
56
|
+
json: async () => ({ ok: true }),
|
|
57
|
+
}));
|
|
58
|
+
globalThis.fetch = mocked;
|
|
59
|
+
const result = await getRandomActiveProvider("production");
|
|
60
|
+
expect(result.provider.url).toBe("https://pronode.prosopo.io");
|
|
129
61
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
expect(selections.address1).toBe(25);
|
|
159
|
-
expect(selections.address2).toBe(75);
|
|
62
|
+
});
|
|
63
|
+
describe("getRandomActiveProvider (single stack ipMode)", () => {
|
|
64
|
+
it("hits ipv4.pronode.prosopo.io/healthz and pins to ipv4.pronodeN", async () => {
|
|
65
|
+
const mocked = mockHealthzFetch("pronode4.prosopo.io");
|
|
66
|
+
const result = await getRandomActiveProvider("production", "ipv4");
|
|
67
|
+
expect(result.provider.url).toBe("https://ipv4.pronode4.prosopo.io");
|
|
68
|
+
expect(mocked).toHaveBeenCalledWith("https://ipv4.pronode.prosopo.io/healthz", expect.any(Object));
|
|
69
|
+
});
|
|
70
|
+
it("hits ipv6 staging /healthz and pins to ipv6.staging-pronodeN", async () => {
|
|
71
|
+
const mocked = mockHealthzFetch("staging-pronode2.prosopo.io");
|
|
72
|
+
const result = await getRandomActiveProvider("staging", "ipv6");
|
|
73
|
+
expect(result.provider.url).toBe("https://ipv6.staging-pronode2.prosopo.io");
|
|
74
|
+
expect(mocked).toHaveBeenCalledWith("https://ipv6.staging.pronode.prosopo.io/healthz", expect.any(Object));
|
|
75
|
+
});
|
|
76
|
+
it("falls back to the ipv4-labelled global URL when ipv4 /healthz fails", async () => {
|
|
77
|
+
mockHealthzFetch("ignored", false, 500);
|
|
78
|
+
const result = await getRandomActiveProvider("production", "ipv4");
|
|
79
|
+
expect(result.provider.url).toBe("https://ipv4.pronode.prosopo.io");
|
|
80
|
+
});
|
|
81
|
+
it("keeps the dual-stack cache and the ipv4 cache separate", async () => {
|
|
82
|
+
const mocked = mockHealthzFetch("pronode9.prosopo.io");
|
|
83
|
+
await getRandomActiveProvider("production");
|
|
84
|
+
await getRandomActiveProvider("production", "ipv4");
|
|
85
|
+
await getRandomActiveProvider("production");
|
|
86
|
+
await getRandomActiveProvider("production", "ipv4");
|
|
87
|
+
expect(mocked).toHaveBeenCalledTimes(2);
|
|
160
88
|
});
|
|
161
89
|
});
|
|
162
|
-
describe("
|
|
90
|
+
describe("getProviders", () => {
|
|
163
91
|
beforeEach(() => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
vi.resetAllMocks();
|
|
167
|
-
vi.resetModules();
|
|
92
|
+
_resetProviderListCache();
|
|
93
|
+
loadBalancer.mockReset();
|
|
168
94
|
});
|
|
169
|
-
it("
|
|
170
|
-
const
|
|
171
|
-
{ address: "
|
|
172
|
-
{ address: "address2", url: "url2", datasetId: "dataset2", weight: 1 },
|
|
173
|
-
];
|
|
174
|
-
loadBalancer.mockResolvedValue(mockProviders);
|
|
175
|
-
const result = await getRandomActiveProvider("development", 1);
|
|
176
|
-
expect(result.providerAccount).toBe("address2");
|
|
177
|
-
expect(result.provider.url).toBe("url2");
|
|
178
|
-
expect(result.provider.datasetId).toBe("dataset2");
|
|
179
|
-
});
|
|
180
|
-
it("loads providers only once when called multiple times", async () => {
|
|
181
|
-
const mockProviders = [
|
|
182
|
-
{ address: "address1", url: "url1", datasetId: "dataset1", weight: 1 },
|
|
95
|
+
it("loads the provider list once and serves repeat callers from cache", async () => {
|
|
96
|
+
const providers = [
|
|
97
|
+
{ address: "5xyz", url: "https://p1", datasetId: "0x0" },
|
|
183
98
|
];
|
|
184
|
-
loadBalancer.mockResolvedValue(
|
|
185
|
-
await
|
|
186
|
-
await
|
|
99
|
+
loadBalancer.mockResolvedValue(providers);
|
|
100
|
+
const first = await getProviders("production");
|
|
101
|
+
const second = await getProviders("production");
|
|
102
|
+
expect(first).toBe(providers);
|
|
103
|
+
expect(second).toBe(providers);
|
|
187
104
|
expect(loadBalancer).toHaveBeenCalledTimes(1);
|
|
188
105
|
});
|
|
189
|
-
it("
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
});
|
|
193
|
-
it("respects provider weights when selecting", async () => {
|
|
194
|
-
const mockProviders = [
|
|
195
|
-
{ address: "address1", url: "url1", datasetId: "dataset1", weight: 1 },
|
|
196
|
-
{ address: "address2", url: "url2", datasetId: "dataset2", weight: 3 },
|
|
106
|
+
it("dedupes concurrent in-flight loads into a single call", async () => {
|
|
107
|
+
const providers = [
|
|
108
|
+
{ address: "5xyz", url: "https://p1", datasetId: "0x0" },
|
|
197
109
|
];
|
|
198
|
-
loadBalancer.mockResolvedValue(
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
expect(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
expect(result2.providerAccount).toBe("address2");
|
|
207
|
-
_resetCache();
|
|
208
|
-
const result3 = await getRandomActiveProvider("development", 3);
|
|
209
|
-
expect(result3.providerAccount).toBe("address2");
|
|
110
|
+
loadBalancer.mockResolvedValue(providers);
|
|
111
|
+
const [a, b] = await Promise.all([
|
|
112
|
+
getProviders("production"),
|
|
113
|
+
getProviders("production"),
|
|
114
|
+
]);
|
|
115
|
+
expect(a).toBe(providers);
|
|
116
|
+
expect(b).toBe(providers);
|
|
117
|
+
expect(loadBalancer).toHaveBeenCalledTimes(1);
|
|
210
118
|
});
|
|
211
|
-
it("
|
|
212
|
-
const
|
|
213
|
-
{ address: "
|
|
214
|
-
{ address: "address2", url: "url2", datasetId: "dataset2", weight: 1 },
|
|
119
|
+
it("does not cache failures — a rejected load is retried on the next call", async () => {
|
|
120
|
+
const providers = [
|
|
121
|
+
{ address: "5xyz", url: "https://p1", datasetId: "0x0" },
|
|
215
122
|
];
|
|
216
|
-
loadBalancer
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
expect(
|
|
123
|
+
loadBalancer
|
|
124
|
+
.mockRejectedValueOnce(new Error("transient"))
|
|
125
|
+
.mockResolvedValueOnce(providers);
|
|
126
|
+
await expect(getProviders("production")).rejects.toThrow("transient");
|
|
127
|
+
const retry = await getProviders("production");
|
|
128
|
+
expect(retry).toBe(providers);
|
|
129
|
+
expect(loadBalancer).toHaveBeenCalledTimes(2);
|
|
222
130
|
});
|
|
223
131
|
});
|
|
224
132
|
//# sourceMappingURL=providers.unit.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.unit.test.js","sourceRoot":"","sources":["../../src/tests/providers.unit.test.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"providers.unit.test.js","sourceRoot":"","sources":["../../src/tests/providers.unit.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/load-balancer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"description": "Provider load balancer",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -37,12 +37,12 @@
|
|
|
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.41",
|
|
41
|
+
"@prosopo/types": "4.9.0",
|
|
42
42
|
"zod": "3.23.8"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@prosopo/config": "3.3.
|
|
45
|
+
"@prosopo/config": "3.3.4",
|
|
46
46
|
"@types/node": "22.10.2",
|
|
47
47
|
"@vitest/coverage-v8": "3.2.4",
|
|
48
48
|
"concurrently": "9.0.1",
|
package/src/balancer.ts
CHANGED
|
@@ -32,17 +32,43 @@ const HardcodedProviderSchema = z.object({
|
|
|
32
32
|
|
|
33
33
|
export type HardcodedProvider = z.infer<typeof HardcodedProviderSchema>;
|
|
34
34
|
|
|
35
|
+
export type IpMode = "ipv4" | "ipv6";
|
|
36
|
+
|
|
37
|
+
// Top-level keys reserved by providerListJson for the ipv4-only / ipv6-only
|
|
38
|
+
// sub-lists. They appear alongside the dual-stack provider entries in the
|
|
39
|
+
// fetched JSON; the converter skips them so existing dual-stack consumers
|
|
40
|
+
// keep working.
|
|
41
|
+
const IP_MODE_KEYS: ReadonlyArray<IpMode> = ["ipv4", "ipv6"];
|
|
42
|
+
|
|
35
43
|
type hostedProviders = Record<string, unknown>;
|
|
36
44
|
|
|
45
|
+
const isProviderRecord = (value: unknown): value is hostedProviders =>
|
|
46
|
+
typeof value === "object" && value !== null;
|
|
47
|
+
|
|
37
48
|
export const convertHostedProvider = (
|
|
38
49
|
provider: hostedProviders,
|
|
50
|
+
ipMode?: IpMode,
|
|
39
51
|
): HardcodedProvider[] => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
const source =
|
|
53
|
+
ipMode && isProviderRecord(provider[ipMode])
|
|
54
|
+
? (provider[ipMode] as hostedProviders)
|
|
55
|
+
: provider;
|
|
56
|
+
|
|
57
|
+
const providers = Object.entries(source)
|
|
58
|
+
.filter(([key]) => !IP_MODE_KEYS.includes(key as IpMode))
|
|
59
|
+
.map(([, value]) => HardcodedProviderSchema.parse(value));
|
|
43
60
|
return providers.sort((a, b) => a.url.localeCompare(b.url));
|
|
44
61
|
};
|
|
45
62
|
|
|
63
|
+
// Strip the leading `ipv4.` / `ipv6.` label that providerListJson prepends for
|
|
64
|
+
// the single-stack sub-lists so callers can derive a stable identity from the
|
|
65
|
+
// provider URL regardless of which ipMode was requested.
|
|
66
|
+
export const stripIpModeLabel = (hostname: string): string =>
|
|
67
|
+
hostname.replace(/^ipv[46]\./, "");
|
|
68
|
+
|
|
69
|
+
export const getProviderHostname = (provider: HardcodedProvider): string =>
|
|
70
|
+
stripIpModeLabel(new URL(provider.url).hostname);
|
|
71
|
+
|
|
46
72
|
export const getLoadBalancerUrl = (environment: EnvironmentTypes): string => {
|
|
47
73
|
if (environment === "production") {
|
|
48
74
|
return "https://provider-list.prosopo.io/";
|