@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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 +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -6,463 +6,469 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
6
6
|
import { createDeferred, nextTick } from "./helpers/deferred.js";
|
|
7
7
|
import { createMockBunProxy } from "./helpers/mock-bun-proxy.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
contentBlockDeltaEvent,
|
|
10
|
+
contentBlockStartEvent,
|
|
11
|
+
contentBlockStopEvent,
|
|
12
|
+
encodeSSEStream,
|
|
13
|
+
makeSSEResponse,
|
|
14
|
+
messageStartEvent,
|
|
15
|
+
messageStopEvent,
|
|
16
16
|
} from "./helpers/sse.js";
|
|
17
17
|
|
|
18
18
|
interface ParentWatcher {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
start(): void;
|
|
20
|
+
stop(): void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
type ParentWatcherFactory = (options: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
parentPid: number;
|
|
25
|
+
onParentExit: (exitCode?: number) => void;
|
|
26
|
+
pollIntervalMs?: number;
|
|
27
|
+
exitCode?: number;
|
|
28
28
|
}) => ParentWatcher;
|
|
29
29
|
|
|
30
30
|
interface BunProxyRequestHandlerOptions {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
fetchImpl: typeof fetch;
|
|
32
|
+
allowHosts?: string[];
|
|
33
|
+
requestTimeoutMs?: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
interface BunProxyProcessRuntimeOptions {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
argv?: string[];
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
exit?: (code?: number) => void;
|
|
40
|
+
parentWatcherFactory?: ParentWatcherFactory;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
interface BunProxyModuleContract {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
createProxyRequestHandler(options: BunProxyRequestHandlerOptions): (request: Request) => Promise<Response>;
|
|
45
|
+
createProxyProcessRuntime?(options: BunProxyProcessRuntimeOptions): ParentWatcher;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const bunProxySourcePath = fileURLToPath(new URL("../bun-proxy.ts", import.meta.url));
|
|
49
49
|
const bunProxySource = readFileSync(bunProxySourcePath, "utf-8");
|
|
50
50
|
|
|
51
51
|
async function loadBunProxyModule(): Promise<BunProxyModuleContract> {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const modulePath = "../bun-proxy.js";
|
|
53
|
+
return (await import(modulePath)) as BunProxyModuleContract;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async function createProxyRequestHandler(
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
fetchImpl: typeof fetch,
|
|
58
|
+
overrides: Partial<BunProxyRequestHandlerOptions> = {},
|
|
59
59
|
): Promise<(request: Request) => Promise<Response>> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
const { createProxyRequestHandler } = await loadBunProxyModule();
|
|
61
|
+
|
|
62
|
+
return createProxyRequestHandler({
|
|
63
|
+
fetchImpl,
|
|
64
|
+
allowHosts: ["api.anthropic.com", "platform.claude.com"],
|
|
65
|
+
requestTimeoutMs: 50,
|
|
66
|
+
...overrides,
|
|
67
|
+
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function makeProxyRequest(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
requestId: number,
|
|
72
|
+
overrides: {
|
|
73
|
+
body?: string;
|
|
74
|
+
headers?: HeadersInit;
|
|
75
|
+
method?: string;
|
|
76
|
+
signal?: AbortSignal;
|
|
77
|
+
targetUrl?: string;
|
|
78
|
+
} = {},
|
|
79
79
|
): Request {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
const headers = new Headers(overrides.headers);
|
|
81
|
+
headers.set("x-proxy-url", overrides.targetUrl ?? "https://api.anthropic.com/v1/messages");
|
|
82
|
+
|
|
83
|
+
if (!headers.has("content-type")) {
|
|
84
|
+
headers.set("content-type", "application/json");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return new Request("http://127.0.0.1/proxy", {
|
|
88
|
+
method: overrides.method ?? "POST",
|
|
89
|
+
headers,
|
|
90
|
+
body: overrides.body ?? JSON.stringify({ requestId }),
|
|
91
|
+
signal: overrides.signal,
|
|
92
|
+
});
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function makeSSETranscript(requestId: number): string {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
96
|
+
return encodeSSEStream([
|
|
97
|
+
messageStartEvent({
|
|
98
|
+
message: {
|
|
99
|
+
id: `msg_${requestId}`,
|
|
100
|
+
type: "message",
|
|
101
|
+
role: "assistant",
|
|
102
|
+
content: [],
|
|
103
|
+
model: "claude-3-opus-20240229",
|
|
104
|
+
stop_reason: null,
|
|
105
|
+
stop_sequence: null,
|
|
106
|
+
usage: {
|
|
107
|
+
input_tokens: 10,
|
|
108
|
+
output_tokens: 0,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
contentBlockStartEvent(0, {
|
|
113
|
+
content_block: {
|
|
114
|
+
type: "text",
|
|
115
|
+
text: "",
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
contentBlockDeltaEvent(0, `stream-${requestId}-a`),
|
|
119
|
+
contentBlockDeltaEvent(0, `stream-${requestId}-b`),
|
|
120
|
+
contentBlockStopEvent(0),
|
|
121
|
+
messageStopEvent(),
|
|
122
|
+
]);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
async function flushMicrotasks(turns = 8): Promise<void> {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
for (let index = 0; index < turns; index += 1) {
|
|
127
|
+
await nextTick();
|
|
128
|
+
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
beforeEach(() => {
|
|
132
|
-
|
|
132
|
+
vi.resetModules();
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
afterEach(() => {
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
vi.restoreAllMocks();
|
|
137
|
+
vi.doUnmock("../parent-pid-watcher.js");
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
describe("bun-proxy parallel request contract (RED)", () => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const responses = await Promise.all(
|
|
148
|
-
Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId))),
|
|
149
|
-
);
|
|
150
|
-
const bodies = await Promise.all(responses.map((response) => response.text()));
|
|
141
|
+
it("single proxy handles 10 concurrent fetches with distinct bodies and responses", async () => {
|
|
142
|
+
const proxy = createMockBunProxy({
|
|
143
|
+
forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
|
|
144
|
+
});
|
|
145
|
+
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
151
146
|
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
const responses = await Promise.all(
|
|
148
|
+
Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId))),
|
|
149
|
+
);
|
|
150
|
+
const bodies = await Promise.all(responses.map((response) => response.text()));
|
|
154
151
|
|
|
155
|
-
|
|
156
|
-
const proxy = createMockBunProxy({
|
|
157
|
-
forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
|
|
152
|
+
expect(bodies).toEqual(Array.from({ length: 10 }, (_, requestId) => JSON.stringify({ requestId })));
|
|
158
153
|
});
|
|
159
|
-
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
160
154
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
155
|
+
it("single proxy handles 50 concurrent fetches with distinct bodies", async () => {
|
|
156
|
+
const proxy = createMockBunProxy({
|
|
157
|
+
forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
|
|
158
|
+
});
|
|
159
|
+
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
165
160
|
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
const responses = await Promise.all(
|
|
162
|
+
Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(requestId))),
|
|
163
|
+
);
|
|
164
|
+
const bodies = await Promise.all(responses.map((response) => response.text()));
|
|
168
165
|
|
|
169
|
-
|
|
170
|
-
const deferredResponses = Array.from({ length: 50 }, () => createDeferred<Response>());
|
|
171
|
-
let nextResponse = 0;
|
|
172
|
-
const proxy = createMockBunProxy({
|
|
173
|
-
forwardToMockFetch: vi.fn(() => deferredResponses[nextResponse++]!.promise),
|
|
166
|
+
expect(bodies).toEqual(Array.from({ length: 50 }, (_, requestId) => JSON.stringify({ requestId })));
|
|
174
167
|
});
|
|
175
|
-
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
176
168
|
|
|
177
|
-
|
|
169
|
+
it("starts all concurrent upstream fetches in parallel without serialization", async () => {
|
|
170
|
+
const deferredResponses = Array.from({ length: 50 }, () => createDeferred<Response>());
|
|
171
|
+
let nextResponse = 0;
|
|
172
|
+
const proxy = createMockBunProxy({
|
|
173
|
+
forwardToMockFetch: vi.fn(() => deferredResponses[nextResponse++]!.promise),
|
|
174
|
+
});
|
|
175
|
+
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
178
176
|
|
|
179
|
-
|
|
177
|
+
const pendingResponses = Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(requestId)));
|
|
180
178
|
|
|
181
|
-
|
|
179
|
+
await flushMicrotasks();
|
|
182
180
|
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
expect(proxy.getInFlightCount()).toBe(50);
|
|
182
|
+
|
|
183
|
+
deferredResponses.forEach((deferred, requestId) => {
|
|
184
|
+
deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await Promise.all(pendingResponses);
|
|
185
188
|
});
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
it("slow request does not head-of-line block siblings", async () => {
|
|
191
|
+
const slowResponse = createDeferred<Response>();
|
|
192
|
+
const proxy = createMockBunProxy({
|
|
193
|
+
forwardToMockFetch: vi.fn(async (_input, init) => {
|
|
194
|
+
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
195
|
+
if (requestId === 0) {
|
|
196
|
+
return slowResponse.promise;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return new Response(`fast-${requestId}`, { status: 200 });
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
203
|
+
|
|
204
|
+
const responses = Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId)));
|
|
205
|
+
const fastBodies = await Promise.all(
|
|
206
|
+
responses.slice(1).map(async (responsePromise) => {
|
|
207
|
+
const response = await responsePromise;
|
|
208
|
+
return response.text();
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
189
211
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
195
|
-
if (requestId === 0) {
|
|
196
|
-
return slowResponse.promise;
|
|
197
|
-
}
|
|
212
|
+
expect(fastBodies).toEqual(Array.from({ length: 9 }, (_, index) => `fast-${index + 1}`));
|
|
213
|
+
expect(proxy.getInFlightCount()).toBe(1);
|
|
214
|
+
|
|
215
|
+
slowResponse.resolve(new Response("fast-0", { status: 200 }));
|
|
198
216
|
|
|
199
|
-
|
|
200
|
-
|
|
217
|
+
const slowBody = await (await responses[0]).text();
|
|
218
|
+
expect(slowBody).toBe("fast-0");
|
|
201
219
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
225
|
-
return makeSSEResponse(makeSSETranscript(requestId));
|
|
226
|
-
}),
|
|
220
|
+
|
|
221
|
+
it("concurrent SSE streams maintain per-stream event ordering", async () => {
|
|
222
|
+
const proxy = createMockBunProxy({
|
|
223
|
+
forwardToMockFetch: vi.fn(async (_input, init) => {
|
|
224
|
+
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
225
|
+
return makeSSEResponse(makeSSETranscript(requestId));
|
|
226
|
+
}),
|
|
227
|
+
});
|
|
228
|
+
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
229
|
+
|
|
230
|
+
const responses = await Promise.all(
|
|
231
|
+
Array.from({ length: 5 }, (_, requestId) => handler(makeProxyRequest(requestId))),
|
|
232
|
+
);
|
|
233
|
+
const transcripts = await Promise.all(responses.map((response) => response.text()));
|
|
234
|
+
|
|
235
|
+
transcripts.forEach((transcript, requestId) => {
|
|
236
|
+
expect(transcript).toContain(`stream-${requestId}-a`);
|
|
237
|
+
expect(transcript).toContain(`stream-${requestId}-b`);
|
|
238
|
+
expect(transcript.indexOf(`stream-${requestId}-a`)).toBeLessThan(
|
|
239
|
+
transcript.indexOf(`stream-${requestId}-b`),
|
|
240
|
+
);
|
|
241
|
+
});
|
|
227
242
|
});
|
|
228
|
-
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
229
243
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
it("upstream error in 1 request does not affect siblings", async () => {
|
|
245
|
+
const proxy = createMockBunProxy({
|
|
246
|
+
forwardToMockFetch: vi.fn(async (_input, init) => {
|
|
247
|
+
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
248
|
+
if (requestId === 3) {
|
|
249
|
+
throw new Error("boom-3");
|
|
250
|
+
}
|
|
234
251
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
252
|
+
return new Response(`ok-${requestId}`, { status: 200 });
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
256
|
+
|
|
257
|
+
const outcomes = await Promise.allSettled(
|
|
258
|
+
Array.from({ length: 10 }, async (_, requestId) => {
|
|
259
|
+
const response = await handler(makeProxyRequest(requestId));
|
|
260
|
+
return {
|
|
261
|
+
status: response.status,
|
|
262
|
+
body: await response.text(),
|
|
263
|
+
};
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
outcomes.forEach((outcome, requestId) => {
|
|
268
|
+
if (requestId === 3) {
|
|
269
|
+
expect(
|
|
270
|
+
outcome.status === "rejected" || (outcome.status === "fulfilled" && outcome.value.status >= 500),
|
|
271
|
+
).toBe(true);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
expect(outcome.status).toBe("fulfilled");
|
|
276
|
+
if (outcome.status === "fulfilled") {
|
|
277
|
+
expect(outcome.value).toEqual({
|
|
278
|
+
status: 200,
|
|
279
|
+
body: `ok-${requestId}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
239
283
|
});
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
|
|
285
|
+
it("upstream timeout of 1 request does not crash the proxy or cascade to later requests", async () => {
|
|
286
|
+
const abortedRequestIds: number[] = [];
|
|
287
|
+
const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
|
|
288
|
+
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
289
|
+
const deferred = createDeferred<Response>();
|
|
290
|
+
|
|
291
|
+
init?.signal?.addEventListener(
|
|
292
|
+
"abort",
|
|
293
|
+
() => {
|
|
294
|
+
abortedRequestIds.push(requestId);
|
|
295
|
+
deferred.reject(init.signal?.reason ?? new DOMException("Timed out", "TimeoutError"));
|
|
296
|
+
},
|
|
297
|
+
{ once: true },
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (requestId !== 0) {
|
|
301
|
+
deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return deferred.promise;
|
|
305
|
+
}) as typeof fetch;
|
|
306
|
+
const handler = await createProxyRequestHandler(fetchImpl, { requestTimeoutMs: 25 });
|
|
307
|
+
|
|
308
|
+
const responses = Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId)));
|
|
309
|
+
const fastStatuses = await Promise.all(
|
|
310
|
+
responses.slice(1).map(async (responsePromise) => {
|
|
311
|
+
const response = await responsePromise;
|
|
312
|
+
return response.status;
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Wait for the 25ms proxy timeout to actually fire on request 0. The
|
|
317
|
+
// previous hard 50ms sleep flaked under host load (husky pre-publish,
|
|
318
|
+
// lint-staged overhead, CI workers) because the abort callback would
|
|
319
|
+
// not have run yet when the assertion fired. Poll for the expected
|
|
320
|
+
// state with a generous upper bound instead of racing a fixed delay.
|
|
321
|
+
const deadline = Date.now() + 500;
|
|
322
|
+
while (abortedRequestIds.length < 1 && Date.now() < deadline) {
|
|
323
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
248
324
|
}
|
|
249
325
|
|
|
250
|
-
|
|
251
|
-
|
|
326
|
+
expect(fastStatuses).toEqual(Array.from({ length: 9 }, () => 200));
|
|
327
|
+
expect(abortedRequestIds).toEqual([0]);
|
|
328
|
+
|
|
329
|
+
await expect(handler(makeProxyRequest(99))).resolves.toBeInstanceOf(Response);
|
|
252
330
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
331
|
+
|
|
332
|
+
it("canceling 1 of 10 requests aborts only that upstream signal and not siblings", async () => {
|
|
333
|
+
const abortedRequestIds: number[] = [];
|
|
334
|
+
const deferredResponses = new Map<number, ReturnType<typeof createDeferred<Response>>>();
|
|
335
|
+
const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
|
|
336
|
+
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
337
|
+
const deferred = createDeferred<Response>();
|
|
338
|
+
|
|
339
|
+
deferredResponses.set(requestId, deferred);
|
|
340
|
+
init?.signal?.addEventListener(
|
|
341
|
+
"abort",
|
|
342
|
+
() => {
|
|
343
|
+
abortedRequestIds.push(requestId);
|
|
344
|
+
deferred.reject(init.signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
345
|
+
},
|
|
346
|
+
{ once: true },
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
return deferred.promise;
|
|
350
|
+
}) as typeof fetch;
|
|
351
|
+
const handler = await createProxyRequestHandler(fetchImpl);
|
|
352
|
+
const controllers = Array.from({ length: 10 }, () => new AbortController());
|
|
353
|
+
const responses = Array.from({ length: 10 }, (_, requestId) =>
|
|
354
|
+
handler(makeProxyRequest(requestId, { signal: controllers[requestId].signal })),
|
|
269
355
|
);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
356
|
+
|
|
357
|
+
await flushMicrotasks();
|
|
358
|
+
controllers[4].abort(new DOMException("client disconnected", "AbortError"));
|
|
359
|
+
|
|
360
|
+
deferredResponses.forEach((deferred, requestId) => {
|
|
361
|
+
if (requestId !== 4) {
|
|
362
|
+
deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
|
|
363
|
+
}
|
|
278
364
|
});
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("upstream timeout of 1 request does not crash the proxy or cascade to later requests", async () => {
|
|
284
|
-
const abortedRequestIds: number[] = [];
|
|
285
|
-
const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
|
|
286
|
-
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
287
|
-
const deferred = createDeferred<Response>();
|
|
288
|
-
|
|
289
|
-
init?.signal?.addEventListener(
|
|
290
|
-
"abort",
|
|
291
|
-
() => {
|
|
292
|
-
abortedRequestIds.push(requestId);
|
|
293
|
-
deferred.reject(init.signal?.reason ?? new DOMException("Timed out", "TimeoutError"));
|
|
294
|
-
},
|
|
295
|
-
{ once: true },
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
if (requestId !== 0) {
|
|
299
|
-
deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return deferred.promise;
|
|
303
|
-
}) as typeof fetch;
|
|
304
|
-
const handler = await createProxyRequestHandler(fetchImpl, { requestTimeoutMs: 25 });
|
|
305
|
-
|
|
306
|
-
const responses = Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId)));
|
|
307
|
-
const fastStatuses = await Promise.all(
|
|
308
|
-
responses.slice(1).map(async (responsePromise) => {
|
|
309
|
-
const response = await responsePromise;
|
|
310
|
-
return response.status;
|
|
311
|
-
}),
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
// Wait for the 25ms proxy timeout to actually fire on request 0. The
|
|
315
|
-
// previous hard 50ms sleep flaked under host load (husky pre-publish,
|
|
316
|
-
// lint-staged overhead, CI workers) because the abort callback would
|
|
317
|
-
// not have run yet when the assertion fired. Poll for the expected
|
|
318
|
-
// state with a generous upper bound instead of racing a fixed delay.
|
|
319
|
-
const deadline = Date.now() + 500;
|
|
320
|
-
while (abortedRequestIds.length < 1 && Date.now() < deadline) {
|
|
321
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
322
|
-
}
|
|
323
365
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
it("canceling 1 of 10 requests aborts only that upstream signal and not siblings", async () => {
|
|
331
|
-
const abortedRequestIds: number[] = [];
|
|
332
|
-
const deferredResponses = new Map<number, ReturnType<typeof createDeferred<Response>>>();
|
|
333
|
-
const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
|
|
334
|
-
const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
|
|
335
|
-
const deferred = createDeferred<Response>();
|
|
336
|
-
|
|
337
|
-
deferredResponses.set(requestId, deferred);
|
|
338
|
-
init?.signal?.addEventListener(
|
|
339
|
-
"abort",
|
|
340
|
-
() => {
|
|
341
|
-
abortedRequestIds.push(requestId);
|
|
342
|
-
deferred.reject(init.signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
343
|
-
},
|
|
344
|
-
{ once: true },
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
return deferred.promise;
|
|
348
|
-
}) as typeof fetch;
|
|
349
|
-
const handler = await createProxyRequestHandler(fetchImpl);
|
|
350
|
-
const controllers = Array.from({ length: 10 }, () => new AbortController());
|
|
351
|
-
const responses = Array.from({ length: 10 }, (_, requestId) =>
|
|
352
|
-
handler(makeProxyRequest(requestId, { signal: controllers[requestId].signal })),
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
await flushMicrotasks();
|
|
356
|
-
controllers[4].abort(new DOMException("client disconnected", "AbortError"));
|
|
357
|
-
|
|
358
|
-
deferredResponses.forEach((deferred, requestId) => {
|
|
359
|
-
if (requestId !== 4) {
|
|
360
|
-
deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
|
|
361
|
-
}
|
|
362
|
-
});
|
|
366
|
+
const outcomes = await Promise.allSettled(
|
|
367
|
+
responses.map(async (responsePromise) => {
|
|
368
|
+
const response = await responsePromise;
|
|
369
|
+
return response.status;
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
363
372
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
expect(outcome).toMatchObject({
|
|
379
|
-
status: "fulfilled",
|
|
380
|
-
value: 200,
|
|
381
|
-
});
|
|
373
|
+
expect(abortedRequestIds).toEqual([4]);
|
|
374
|
+
outcomes.forEach((outcome, requestId) => {
|
|
375
|
+
if (requestId === 4) {
|
|
376
|
+
expect(outcome.status === "rejected" || (outcome.status === "fulfilled" && outcome.value >= 400)).toBe(
|
|
377
|
+
true,
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
expect(outcome).toMatchObject({
|
|
383
|
+
status: "fulfilled",
|
|
384
|
+
value: 200,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
382
387
|
});
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
it("releases all in-flight bookkeeping after repeated bursts to keep memory bounded", async () => {
|
|
419
|
-
const proxy = createMockBunProxy({
|
|
420
|
-
forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
|
|
388
|
+
|
|
389
|
+
it("client disconnect aborts the upstream fetch signal", async () => {
|
|
390
|
+
let upstreamSignal: AbortSignal | null | undefined;
|
|
391
|
+
const upstreamResponse = createDeferred<Response>();
|
|
392
|
+
const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
|
|
393
|
+
upstreamSignal = init?.signal;
|
|
394
|
+
init?.signal?.addEventListener(
|
|
395
|
+
"abort",
|
|
396
|
+
() => {
|
|
397
|
+
upstreamResponse.reject(
|
|
398
|
+
init.signal?.reason ?? new DOMException("client disconnected", "AbortError"),
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
{ once: true },
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
return upstreamResponse.promise;
|
|
405
|
+
}) as typeof fetch;
|
|
406
|
+
const handler = await createProxyRequestHandler(fetchImpl);
|
|
407
|
+
const controller = new AbortController();
|
|
408
|
+
const responsePromise = handler(makeProxyRequest(123, { signal: controller.signal }));
|
|
409
|
+
|
|
410
|
+
await flushMicrotasks();
|
|
411
|
+
|
|
412
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
413
|
+
expect(upstreamSignal).toBeDefined();
|
|
414
|
+
expect(upstreamSignal?.aborted).toBe(false);
|
|
415
|
+
|
|
416
|
+
controller.abort(new DOMException("client disconnected", "AbortError"));
|
|
417
|
+
|
|
418
|
+
await expect(responsePromise).resolves.toMatchObject({ status: 499 });
|
|
419
|
+
expect(upstreamSignal?.aborted).toBe(true);
|
|
420
|
+
expect(upstreamSignal?.reason).toBeInstanceOf(DOMException);
|
|
421
|
+
expect((upstreamSignal?.reason as DOMException | undefined)?.name).toBe("AbortError");
|
|
421
422
|
});
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
expect(bunProxySource).not.toMatch(/^let\s+/m);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it("starts a parent-PID watcher so the subprocess exits when the parent dies", async () => {
|
|
440
|
-
const parentWatcher = {
|
|
441
|
-
start: vi.fn(),
|
|
442
|
-
stop: vi.fn(),
|
|
443
|
-
} satisfies ParentWatcher;
|
|
444
|
-
const parentWatcherFactory = vi.fn(({ onParentExit }: Parameters<ParentWatcherFactory>[0]) => {
|
|
445
|
-
onParentExit(0);
|
|
446
|
-
return parentWatcher;
|
|
423
|
+
|
|
424
|
+
it("releases all in-flight bookkeeping after repeated bursts to keep memory bounded", async () => {
|
|
425
|
+
const proxy = createMockBunProxy({
|
|
426
|
+
forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
|
|
427
|
+
});
|
|
428
|
+
const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
|
|
429
|
+
|
|
430
|
+
for (let burst = 0; burst < 3; burst += 1) {
|
|
431
|
+
const responses = await Promise.all(
|
|
432
|
+
Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(burst * 100 + requestId))),
|
|
433
|
+
);
|
|
434
|
+
await Promise.all(responses.map((response) => response.text()));
|
|
435
|
+
expect(proxy.getInFlightCount()).toBe(0);
|
|
436
|
+
}
|
|
447
437
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
argv: ["bun", "run", "bun-proxy.ts", "--parent-pid", "4242"],
|
|
454
|
-
fetchImpl: proxy.child.forwardFetch as typeof fetch,
|
|
455
|
-
exit,
|
|
456
|
-
parentWatcherFactory,
|
|
438
|
+
|
|
439
|
+
it("ties the upstream fetch to the incoming request signal without pre-fetch body awaits or mutable globals", () => {
|
|
440
|
+
expect(bunProxySource).toMatch(/AbortSignal\.any\s*\(\s*\[\s*req\.signal/i);
|
|
441
|
+
expect(bunProxySource).not.toMatch(/await\s+req\.arrayBuffer\s*\(/);
|
|
442
|
+
expect(bunProxySource).not.toMatch(/^let\s+/m);
|
|
457
443
|
});
|
|
458
444
|
|
|
459
|
-
|
|
445
|
+
it("starts a parent-PID watcher so the subprocess exits when the parent dies", async () => {
|
|
446
|
+
const parentWatcher = {
|
|
447
|
+
start: vi.fn(),
|
|
448
|
+
stop: vi.fn(),
|
|
449
|
+
} satisfies ParentWatcher;
|
|
450
|
+
const parentWatcherFactory = vi.fn(({ onParentExit }: Parameters<ParentWatcherFactory>[0]) => {
|
|
451
|
+
onParentExit(0);
|
|
452
|
+
return parentWatcher;
|
|
453
|
+
});
|
|
454
|
+
const exit = vi.fn();
|
|
455
|
+
const proxy = createMockBunProxy();
|
|
456
|
+
const { createProxyProcessRuntime } = await loadBunProxyModule();
|
|
457
|
+
|
|
458
|
+
const runtime = createProxyProcessRuntime?.({
|
|
459
|
+
argv: ["bun", "run", "bun-proxy.ts", "--parent-pid", "4242"],
|
|
460
|
+
fetchImpl: proxy.child.forwardFetch as typeof fetch,
|
|
461
|
+
exit,
|
|
462
|
+
parentWatcherFactory,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
runtime?.start();
|
|
460
466
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
467
|
+
expect(parentWatcherFactory).toHaveBeenCalledWith(
|
|
468
|
+
expect.objectContaining({
|
|
469
|
+
parentPid: 4242,
|
|
470
|
+
}),
|
|
471
|
+
);
|
|
472
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
473
|
+
});
|
|
468
474
|
});
|