185 lines
5.7 KiB
TypeScript
185 lines
5.7 KiB
TypeScript
import { describe, test, expect, mock } from "bun:test";
|
|
import { attachCwConcurrencyLimiter } from "../../src/modules/cw-utils/cwConcurrencyLimiter";
|
|
|
|
/**
|
|
* Build a minimal fake Axios instance with interceptor registration.
|
|
* Collect registered interceptors so we can invoke them in tests.
|
|
*/
|
|
function createMockAxios() {
|
|
const requestHandlers: Array<(config: any) => any> = [];
|
|
const responseSuccessHandlers: Array<(res: any) => any> = [];
|
|
const responseErrorHandlers: Array<(err: any) => any> = [];
|
|
|
|
return {
|
|
interceptors: {
|
|
request: {
|
|
use(fn: (config: any) => any) {
|
|
requestHandlers.push(fn);
|
|
},
|
|
},
|
|
response: {
|
|
use(onSuccess: (res: any) => any, onError: (err: any) => any) {
|
|
responseSuccessHandlers.push(onSuccess);
|
|
responseErrorHandlers.push(onError);
|
|
},
|
|
},
|
|
},
|
|
_requestHandlers: requestHandlers,
|
|
_responseSuccessHandlers: responseSuccessHandlers,
|
|
_responseErrorHandlers: responseErrorHandlers,
|
|
};
|
|
}
|
|
|
|
describe("attachCwConcurrencyLimiter", () => {
|
|
test("attaches request and response interceptors", () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any);
|
|
expect(api._requestHandlers).toHaveLength(1);
|
|
expect(api._responseSuccessHandlers).toHaveLength(1);
|
|
expect(api._responseErrorHandlers).toHaveLength(1);
|
|
});
|
|
|
|
test("request interceptor resolves immediately when under limit", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any, 2);
|
|
const config = { url: "/test" };
|
|
const result = await api._requestHandlers[0](config);
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
test("response success interceptor passes through response", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any, 2);
|
|
// Acquire a slot first
|
|
await api._requestHandlers[0]({});
|
|
const response = { data: "ok", status: 200 };
|
|
const result = api._responseSuccessHandlers[0](response);
|
|
expect(result).toEqual(response);
|
|
});
|
|
|
|
test("response error interceptor rejects with the error and releases slot", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any, 2);
|
|
// Acquire a slot
|
|
await api._requestHandlers[0]({});
|
|
const error = new Error("fail");
|
|
try {
|
|
await api._responseErrorHandlers[0](error);
|
|
expect(true).toBe(false); // should not reach
|
|
} catch (e) {
|
|
expect(e).toBe(error);
|
|
}
|
|
});
|
|
|
|
test("queues requests when at max concurrency", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any, 1);
|
|
|
|
// First request acquires the single slot
|
|
await api._requestHandlers[0]({ id: 1 });
|
|
|
|
// Second request should be queued (not resolved yet)
|
|
let secondResolved = false;
|
|
const secondPromise = api._requestHandlers[0]({ id: 2 }).then(
|
|
(config: any) => {
|
|
secondResolved = true;
|
|
return config;
|
|
},
|
|
);
|
|
|
|
// Give the event loop a tick — second should still be pending
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
expect(secondResolved).toBe(false);
|
|
|
|
// Release the first slot via response handler
|
|
api._responseSuccessHandlers[0]({ status: 200 });
|
|
|
|
// Now the second should resolve
|
|
const result = await secondPromise;
|
|
expect(secondResolved).toBe(true);
|
|
expect(result).toEqual({ id: 2 });
|
|
});
|
|
|
|
test("multiple requests under limit all proceed immediately", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any, 3);
|
|
|
|
const results = await Promise.all([
|
|
api._requestHandlers[0]({ id: 1 }),
|
|
api._requestHandlers[0]({ id: 2 }),
|
|
api._requestHandlers[0]({ id: 3 }),
|
|
]);
|
|
|
|
expect(results).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
|
});
|
|
|
|
test("FIFO ordering: queued requests resolve in order", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any, 1);
|
|
|
|
// Fill the single slot
|
|
await api._requestHandlers[0]({ id: 1 });
|
|
|
|
const order: number[] = [];
|
|
|
|
const p2 = api._requestHandlers[0]({ id: 2 }).then(() => order.push(2));
|
|
const p3 = api._requestHandlers[0]({ id: 3 }).then(() => order.push(3));
|
|
|
|
// Release slot → should wake request 2
|
|
api._responseSuccessHandlers[0]({});
|
|
await p2;
|
|
|
|
// Release again → should wake request 3
|
|
api._responseSuccessHandlers[0]({});
|
|
await p3;
|
|
|
|
expect(order).toEqual([2, 3]);
|
|
});
|
|
|
|
test("error release also unblocks queued requests", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any, 1);
|
|
|
|
await api._requestHandlers[0]({ id: 1 });
|
|
|
|
let secondResolved = false;
|
|
const secondPromise = api._requestHandlers[0]({ id: 2 }).then(() => {
|
|
secondResolved = true;
|
|
});
|
|
|
|
// Release via error path
|
|
try {
|
|
await api._responseErrorHandlers[0](new Error("fail"));
|
|
} catch {}
|
|
|
|
await secondPromise;
|
|
expect(secondResolved).toBe(true);
|
|
});
|
|
|
|
test("defaults to max 6 concurrency", async () => {
|
|
const api = createMockAxios();
|
|
attachCwConcurrencyLimiter(api as any); // default max = 6
|
|
|
|
// 6 requests should all proceed immediately
|
|
const promises = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
promises.push(api._requestHandlers[0]({ id: i }));
|
|
}
|
|
const results = await Promise.all(promises);
|
|
expect(results).toHaveLength(6);
|
|
|
|
// 7th should queue
|
|
let seventhResolved = false;
|
|
const seventh = api._requestHandlers[0]({ id: 7 }).then(() => {
|
|
seventhResolved = true;
|
|
});
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
expect(seventhResolved).toBe(false);
|
|
|
|
// Release one to unblock
|
|
api._responseSuccessHandlers[0]({});
|
|
await seventh;
|
|
expect(seventhResolved).toBe(true);
|
|
});
|
|
});
|