quickbase-js 0.1.0-beta.12 → 0.1.0-beta.13
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/CHANGELOG.md +12 -0
- package/README.md +48 -18
- package/dist/esm/quickbase.d.ts +12 -3
- package/dist/esm/quickbase.js +416 -49
- package/dist/esm/quickbase.js.map +1 -1
- package/dist/esm/quickbase.min.js +1 -1
- package/dist/esm/quickbase.min.js.map +1 -1
- package/dist/umd/quickbase.umd.js +416 -49
- package/dist/umd/quickbase.umd.js.map +1 -1
- package/dist/umd/quickbase.umd.min.js +1 -1
- package/dist/umd/quickbase.umd.min.js.map +1 -1
- package/package.json +3 -1
- package/src/BurstAwareThrottleBucket.ts +57 -0
- package/src/{ThrottleBucket.ts → FlowThrottleBucket.ts} +5 -35
- package/src/RateThrottleBucket.ts +5 -0
- package/src/Semaphore.ts +33 -0
- package/src/authorizationStrategy.ts +12 -1
- package/src/generated-unified/QuickbaseClient.ts +1 -1
- package/src/invokeMethod.ts +94 -16
- package/src/pagination.ts +429 -0
- package/src/quickbaseClient.ts +56 -28
- package/src/rateLimiter.ts +3 -3
- package/tests/vitest/qb/quickbase-js/BurstAwareThrottleBucket.test.ts +125 -0
- package/tests/vitest/qb/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +1 -1
- package/tests/vitest/qb/quickbase-js/paginationSkip.test.ts +111 -0
- package/tests/vitest/qb/records/runQuery.test.ts +1 -2
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +7 -10
- package/tests/vitest/unit/quickbase-js/BurstAwareThrottleBucket.test.ts +108 -0
- package/tests/vitest/unit/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +5 -5
- package/tests/vitest/unit/quickbase-js/pagination.test.ts +251 -0
- package/tests/vitest/unit/{quickbaseClientHttpMethods.test.ts → quickbase-js/quickbaseClientHttpMethods.test.ts} +13 -11
- package/tests/vitest/unit/records/runQuery.test.ts +3 -4
- package/tests/vitest/unit/umd.test.ts +0 -8
- /package/tests/vitest/unit/{rateLimit.test.ts → quickbase-js/rateLimit.test.ts} +0 -0
- /package/tests/vitest/unit/{tokenLifespan.test.ts → quickbase-js/tokenLifespan.test.ts} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// tests/vitest/qb/BurstAwareThrottleBucket.test.ts
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { quickbase } from "../../../../src/quickbaseClient";
|
|
4
|
+
|
|
5
|
+
// Set this to true to run the slow test with 80 burst and 100 calls
|
|
6
|
+
const runSlowTests = false;
|
|
7
|
+
|
|
8
|
+
describe("QuickbaseClient with BurstAwareThrottleBucket Integration Tests", () => {
|
|
9
|
+
// Helper function to reduce duplication
|
|
10
|
+
const runThrottleTest = async (
|
|
11
|
+
burst: number,
|
|
12
|
+
windowSeconds: number,
|
|
13
|
+
callCount: number
|
|
14
|
+
) => {
|
|
15
|
+
const mockFetch = async (
|
|
16
|
+
url: string,
|
|
17
|
+
options: RequestInit
|
|
18
|
+
): Promise<Response> => {
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
20
|
+
return new Response(JSON.stringify({ id: "mock-app" }), {
|
|
21
|
+
status: 200,
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const qbClient = quickbase({
|
|
27
|
+
realm: "test",
|
|
28
|
+
userToken: "mock-token",
|
|
29
|
+
throttle: { type: "burst-aware", burst, windowSeconds },
|
|
30
|
+
fetchApi: mockFetch,
|
|
31
|
+
debug: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const throttleTimes: number[] = [];
|
|
35
|
+
const startTimes: number[] = new Array(callCount).fill(0);
|
|
36
|
+
const completionTimes: number[] = [];
|
|
37
|
+
const recordTime = async (index: number) => {
|
|
38
|
+
const preThrottle = Date.now();
|
|
39
|
+
throttleTimes[index] = preThrottle;
|
|
40
|
+
await qbClient.getApp({
|
|
41
|
+
appId: "mock-app-id",
|
|
42
|
+
startTime: startTimes[index],
|
|
43
|
+
});
|
|
44
|
+
completionTimes[index] = Date.now();
|
|
45
|
+
startTimes[index] = startTimes[index] || Date.now() - 500;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const promises = [];
|
|
50
|
+
for (let i = 0; i < callCount; i++) {
|
|
51
|
+
promises.push(recordTime(i));
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
53
|
+
}
|
|
54
|
+
await Promise.all(promises);
|
|
55
|
+
|
|
56
|
+
const relativeThrottleTimes = throttleTimes.map((t) => t - startTime);
|
|
57
|
+
const relativeStartTimes = startTimes.map((t) => t - startTime);
|
|
58
|
+
const durations = completionTimes.map((t) => t - startTime);
|
|
59
|
+
const timings = completionTimes.map((t, i) => t - startTimes[i]);
|
|
60
|
+
|
|
61
|
+
console.log("Relative Throttle Times:", relativeThrottleTimes.slice(0, 10));
|
|
62
|
+
console.log("Relative Start Times:", relativeStartTimes.slice(0, 10));
|
|
63
|
+
console.log("Durations:", durations.slice(0, 10));
|
|
64
|
+
console.log("Timings:", timings.slice(0, 10));
|
|
65
|
+
console.log("Total Duration:", Date.now() - startTime);
|
|
66
|
+
|
|
67
|
+
return { totalDuration: Date.now() - startTime, durations, timings };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
it("handles concurrent API calls with burst 3 and 0.5s window", async () => {
|
|
71
|
+
const { totalDuration, durations, timings } = await runThrottleTest(
|
|
72
|
+
3,
|
|
73
|
+
0.5,
|
|
74
|
+
6
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(totalDuration).toBeGreaterThanOrEqual(1000);
|
|
78
|
+
expect(totalDuration).toBeLessThan(1200);
|
|
79
|
+
|
|
80
|
+
expect(durations.length).toBe(6);
|
|
81
|
+
expect(durations.slice(0, 3).every((d) => d >= 500 && d < 550)).toBe(true);
|
|
82
|
+
|
|
83
|
+
const sortedDurations = [...durations].sort((a, b) => a - b);
|
|
84
|
+
expect(sortedDurations[3]).toBeGreaterThanOrEqual(1000);
|
|
85
|
+
expect(sortedDurations[3]).toBeLessThan(1100);
|
|
86
|
+
expect(sortedDurations[4]).toBeGreaterThanOrEqual(1000);
|
|
87
|
+
expect(sortedDurations[4]).toBeLessThan(1100);
|
|
88
|
+
expect(sortedDurations[5]).toBeGreaterThanOrEqual(1000);
|
|
89
|
+
expect(sortedDurations[5]).toBeLessThan(1100);
|
|
90
|
+
|
|
91
|
+
expect(timings.every((t) => t >= 490 && t < 560)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Optional test for 80 burst - set runSlowTests = true to enable
|
|
95
|
+
if (runSlowTests) {
|
|
96
|
+
it(
|
|
97
|
+
"handles 100 concurrent API calls with burst 80 and 10s window",
|
|
98
|
+
{ timeout: 12000 },
|
|
99
|
+
async () => {
|
|
100
|
+
const { totalDuration, durations, timings } = await runThrottleTest(
|
|
101
|
+
80,
|
|
102
|
+
10,
|
|
103
|
+
100
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(totalDuration).toBeGreaterThanOrEqual(10500);
|
|
107
|
+
expect(totalDuration).toBeLessThan(11500);
|
|
108
|
+
|
|
109
|
+
expect(durations.length).toBe(100);
|
|
110
|
+
expect(durations.slice(0, 80).every((d) => d >= 500 && d < 600)).toBe(
|
|
111
|
+
true
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const sortedDurations = [...durations].sort((a, b) => a - b);
|
|
115
|
+
expect(
|
|
116
|
+
sortedDurations.slice(80).every((d) => d >= 10500 && d < 10600)
|
|
117
|
+
).toBe(true);
|
|
118
|
+
|
|
119
|
+
expect(timings.every((t) => t >= 490 && t < 560)).toBe(true);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
it.skip("handles 100 concurrent API calls with burst 80 and 10s window", () => {});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// tests/vitest/qb/ThrottleBucket.test.ts
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
|
-
import { quickbase } from "
|
|
3
|
+
import { quickbase } from "../../../../src/quickbaseClient";
|
|
4
4
|
|
|
5
5
|
describe("QuickbaseClient with ConcurrentThrottleBucket Integration Tests", () => {
|
|
6
6
|
it("handles concurrent API calls with burst and rate limiting in strict order", async () => {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createClient,
|
|
4
|
+
QB_TABLE_ID_1,
|
|
5
|
+
QB_REALM,
|
|
6
|
+
QB_USER_TOKEN,
|
|
7
|
+
} from "@tests/setup";
|
|
8
|
+
import { QuickbaseClient } from "@/quickbaseClient";
|
|
9
|
+
import {
|
|
10
|
+
RunQueryRequest,
|
|
11
|
+
RunQueryRequestOptions,
|
|
12
|
+
RunQuery200Response,
|
|
13
|
+
} from "@/generated/models";
|
|
14
|
+
|
|
15
|
+
// Extend RunQueryRequestOptions to include sortBy (for reference, not used here)
|
|
16
|
+
interface ExtendedRunQueryRequestOptions extends RunQueryRequestOptions {
|
|
17
|
+
sortBy?: Array<{ fieldId: number; order: "ASC" | "DESC" }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("QuickbaseClient Integration - Pagination with Skip", () => {
|
|
21
|
+
let qb: QuickbaseClient;
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
qb = createClient(undefined, {
|
|
25
|
+
realm: QB_REALM,
|
|
26
|
+
userToken: QB_USER_TOKEN,
|
|
27
|
+
debug: true,
|
|
28
|
+
autoPaginate: true, // Enable library's automatic pagination
|
|
29
|
+
});
|
|
30
|
+
console.log("[beforeAll] Client initialized");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it(
|
|
34
|
+
"fetches full dataset with auto-pagination and heavy data",
|
|
35
|
+
{ timeout: 30000 }, // 30s for multiple calls
|
|
36
|
+
async () => {
|
|
37
|
+
const queryOptions: ExtendedRunQueryRequestOptions = {
|
|
38
|
+
skip: 0, // Start at beginning, no top specified
|
|
39
|
+
// sortBy removed to test without ordering
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const queryRequest: RunQueryRequest = {
|
|
43
|
+
from: QB_TABLE_ID_1,
|
|
44
|
+
select: [3, 822, 823, 824, 825, 6], // FID 3 for Record ID
|
|
45
|
+
options: queryOptions,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
console.log(
|
|
50
|
+
"[Test Start] Query request with auto-pagination:",
|
|
51
|
+
queryRequest
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const response = (await qb.runQuery({
|
|
56
|
+
body: queryRequest,
|
|
57
|
+
})) as RunQuery200Response & {
|
|
58
|
+
data: Array<{ [key: string]: { value: any } }>;
|
|
59
|
+
metadata: {
|
|
60
|
+
totalRecords: number;
|
|
61
|
+
numRecords: number;
|
|
62
|
+
numFields: number;
|
|
63
|
+
skip: number;
|
|
64
|
+
top?: number;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const duration = Date.now() - startTime;
|
|
69
|
+
console.log(`[Test Complete] Response received in ${duration}ms`, {
|
|
70
|
+
recordCount: response.data.length,
|
|
71
|
+
totalRecords: response.metadata.totalRecords,
|
|
72
|
+
defaultTop: response.metadata.top,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Assertions
|
|
76
|
+
expect(response.data).toBeDefined();
|
|
77
|
+
expect(response.data.length).toBeGreaterThan(2429); // More than initial cap, confirming pagination
|
|
78
|
+
expect(response.data.length).toEqual(response.metadata.totalRecords); // Full dataset fetched
|
|
79
|
+
expect(response.metadata).toBeDefined();
|
|
80
|
+
// Removed expect(response.metadata.totalRecords).toBe(5000)
|
|
81
|
+
expect(response.metadata.totalRecords).toBeGreaterThanOrEqual(5000); // Flexible minimum, allows growth
|
|
82
|
+
expect(response.metadata.numFields).toBe(6); // 6 fields with FID 3
|
|
83
|
+
expect(response.metadata.skip).toBe(0); // Initial skip
|
|
84
|
+
|
|
85
|
+
const sampleRecord = response.data[0];
|
|
86
|
+
console.log("[Debug] Sample record:", sampleRecord);
|
|
87
|
+
expect(sampleRecord["6"]?.value).toBeDefined();
|
|
88
|
+
expect(typeof sampleRecord["6"]?.value).toBe("string");
|
|
89
|
+
expect(sampleRecord["6"]?.value.length).toBeGreaterThan(9000); // ~10KB
|
|
90
|
+
expect(sampleRecord["3"]?.value).toBeDefined(); // Check FID 3 exists
|
|
91
|
+
expect(typeof sampleRecord["3"]?.value).toBe("number"); // Record ID is numeric
|
|
92
|
+
|
|
93
|
+
// Check for duplicates using Record ID (FID 3)
|
|
94
|
+
const recordIds = response.data.map(
|
|
95
|
+
(record: { [key: string]: { value: any } }) => record["3"]?.value
|
|
96
|
+
);
|
|
97
|
+
const uniqueRecordIds = new Set(recordIds);
|
|
98
|
+
expect(uniqueRecordIds.size).toEqual(response.data.length); // No duplicates
|
|
99
|
+
expect(uniqueRecordIds.size).toEqual(response.metadata.totalRecords); // All records unique
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
`[Integration Test] Fetched ${response.data.length} records in ${duration}ms, totalRecords: ${response.metadata.totalRecords}`
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const duration = Date.now() - startTime;
|
|
106
|
+
console.error("[Test Error] Failed after", duration, "ms:", error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// tests/vitest/qb/records/runQuery.test.ts
|
|
2
1
|
import { describe, it, expect } from "vitest";
|
|
3
2
|
import { createClient, QB_TABLE_ID_1 } from "../../../setup.ts";
|
|
4
3
|
import {
|
|
@@ -128,7 +127,7 @@ describe("QuickbaseClient Integration - runQuery", () => {
|
|
|
128
127
|
numFields: 2,
|
|
129
128
|
numRecords: 1,
|
|
130
129
|
skip: 0,
|
|
131
|
-
top: 1,
|
|
130
|
+
// Removed top: 1, as it should be undefined in the final response
|
|
132
131
|
totalRecords: expect.any(Number),
|
|
133
132
|
});
|
|
134
133
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// tests/vitest/unit/auth/retryUserToken401.test.ts
|
|
2
|
-
|
|
3
1
|
import { describe, it, expect, vi } from "vitest";
|
|
4
2
|
import {
|
|
5
3
|
createClient,
|
|
@@ -10,7 +8,6 @@ import {
|
|
|
10
8
|
|
|
11
9
|
describe("QuickbaseClient Unit - User Token Retry on 401", () => {
|
|
12
10
|
it("retries with the same user token on 401 and succeeds", async () => {
|
|
13
|
-
const userToken = "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3";
|
|
14
11
|
mockFetch
|
|
15
12
|
.mockResolvedValueOnce(
|
|
16
13
|
new Response(JSON.stringify({ message: "Unauthorized" }), {
|
|
@@ -26,7 +23,7 @@ describe("QuickbaseClient Unit - User Token Retry on 401", () => {
|
|
|
26
23
|
const consoleSpy = vi.spyOn(console, "log");
|
|
27
24
|
const client = createClient(mockFetch, {
|
|
28
25
|
realm: QB_REALM,
|
|
29
|
-
userToken,
|
|
26
|
+
userToken: "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
|
|
30
27
|
useTempTokens: false,
|
|
31
28
|
debug: true,
|
|
32
29
|
});
|
|
@@ -41,7 +38,8 @@ describe("QuickbaseClient Unit - User Token Retry on 401", () => {
|
|
|
41
38
|
expect.objectContaining({
|
|
42
39
|
method: "GET",
|
|
43
40
|
headers: expect.objectContaining({
|
|
44
|
-
Authorization:
|
|
41
|
+
Authorization:
|
|
42
|
+
"QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
|
|
45
43
|
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
46
44
|
"Content-Type": "application/json",
|
|
47
45
|
}),
|
|
@@ -53,17 +51,16 @@ describe("QuickbaseClient Unit - User Token Retry on 401", () => {
|
|
|
53
51
|
expect.objectContaining({
|
|
54
52
|
method: "GET",
|
|
55
53
|
headers: expect.objectContaining({
|
|
56
|
-
Authorization:
|
|
54
|
+
Authorization:
|
|
55
|
+
"QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
|
|
57
56
|
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
58
57
|
"Content-Type": "application/json",
|
|
59
58
|
}),
|
|
60
59
|
})
|
|
61
60
|
);
|
|
62
61
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
66
|
-
"[getFields] Retrying with token: b9f3pk_q4j..."
|
|
62
|
+
"Retrying getFields with existing user token: b9f3pk_q4j..."
|
|
67
63
|
);
|
|
64
|
+
consoleSpy.mockRestore();
|
|
68
65
|
});
|
|
69
66
|
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// test/vitest/unit/BurstAwareThrottleBucket.test.ts
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { BurstAwareThrottleBucket } from "@/BurstAwareThrottleBucket";
|
|
4
|
+
|
|
5
|
+
describe("BurstAwareThrottleBucket Unit Tests", () => {
|
|
6
|
+
it("allows burst capacity immediately", async () => {
|
|
7
|
+
const bucket = new BurstAwareThrottleBucket({
|
|
8
|
+
maxTokens: 3,
|
|
9
|
+
windowSeconds: 10,
|
|
10
|
+
});
|
|
11
|
+
const startTime = Date.now();
|
|
12
|
+
|
|
13
|
+
await Promise.all([
|
|
14
|
+
bucket.acquire().then(() => bucket.release()),
|
|
15
|
+
bucket.acquire().then(() => bucket.release()),
|
|
16
|
+
bucket.acquire().then(() => bucket.release()),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const duration = Date.now() - startTime;
|
|
20
|
+
expect(duration).toBeLessThan(50);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("delays calls beyond burst until window reset", async () => {
|
|
24
|
+
const bucket = new BurstAwareThrottleBucket({
|
|
25
|
+
maxTokens: 3,
|
|
26
|
+
windowSeconds: 0.5,
|
|
27
|
+
});
|
|
28
|
+
const timings: number[] = [];
|
|
29
|
+
const recordTime = async () => {
|
|
30
|
+
await bucket.acquire();
|
|
31
|
+
timings.push(Date.now());
|
|
32
|
+
bucket.release();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const startTime = Date.now();
|
|
36
|
+
await Promise.all([recordTime(), recordTime(), recordTime(), recordTime()]);
|
|
37
|
+
|
|
38
|
+
const durations = timings.map((t) => t - startTime);
|
|
39
|
+
expect(durations.length).toBe(4);
|
|
40
|
+
expect(durations[0]).toBeLessThan(50);
|
|
41
|
+
expect(durations[1]).toBeLessThan(50);
|
|
42
|
+
expect(durations[2]).toBeLessThan(50);
|
|
43
|
+
expect(durations[3]).toBeGreaterThanOrEqual(450);
|
|
44
|
+
expect(durations[3]).toBeLessThan(600); // Relaxed from 550 to 600
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("maintains burst limit over sustained calls with window", async () => {
|
|
48
|
+
const bucket = new BurstAwareThrottleBucket({
|
|
49
|
+
maxTokens: 2,
|
|
50
|
+
windowSeconds: 0.5,
|
|
51
|
+
});
|
|
52
|
+
const timings: number[] = [];
|
|
53
|
+
const recordTime = async () => {
|
|
54
|
+
await bucket.acquire();
|
|
55
|
+
timings.push(Date.now());
|
|
56
|
+
bucket.release();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
await Promise.all([
|
|
61
|
+
recordTime(),
|
|
62
|
+
recordTime(),
|
|
63
|
+
recordTime(),
|
|
64
|
+
recordTime(),
|
|
65
|
+
recordTime(),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const durations = timings.map((t) => t - startTime);
|
|
69
|
+
expect(durations.length).toBe(5);
|
|
70
|
+
expect(durations[0]).toBeLessThan(50);
|
|
71
|
+
expect(durations[1]).toBeLessThan(50);
|
|
72
|
+
expect(durations[2]).toBeGreaterThanOrEqual(450);
|
|
73
|
+
expect(durations[2]).toBeLessThan(600); // Relaxed from 550 to 600
|
|
74
|
+
expect(durations[3]).toBeGreaterThanOrEqual(450);
|
|
75
|
+
expect(durations[3]).toBeLessThan(600); // Relaxed from 550 to 600
|
|
76
|
+
expect(durations[4]).toBeGreaterThanOrEqual(950);
|
|
77
|
+
expect(durations[4]).toBeLessThan(1100); // Relaxed from 1050 to 1100
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles high concurrency without exceeding burst within window", async () => {
|
|
81
|
+
const bucket = new BurstAwareThrottleBucket({
|
|
82
|
+
maxTokens: 3,
|
|
83
|
+
windowSeconds: 0.5,
|
|
84
|
+
});
|
|
85
|
+
const timings: number[] = [];
|
|
86
|
+
const recordTime = async () => {
|
|
87
|
+
await bucket.acquire();
|
|
88
|
+
timings.push(Date.now());
|
|
89
|
+
bucket.release();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
await Promise.all(
|
|
94
|
+
Array(10)
|
|
95
|
+
.fill(null)
|
|
96
|
+
.map(() => recordTime())
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const durations = timings.map((t) => t - startTime);
|
|
100
|
+
expect(durations.length).toBe(10);
|
|
101
|
+
expect(durations.slice(0, 3).every((d) => d < 50)).toBe(true);
|
|
102
|
+
for (let i = 3; i < 10; i++) {
|
|
103
|
+
const expectedDelay = Math.floor((i - 3) / 3) * 500 + 450;
|
|
104
|
+
expect(durations[i]).toBeGreaterThanOrEqual(expectedDelay - 50);
|
|
105
|
+
expect(durations[i]).toBeLessThan(expectedDelay + 100); // Relaxed from 50 to 100
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// test/vitest/unit/ThrottleBucket.test.ts
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { FlowThrottleBucket } from "@/FlowThrottleBucket"; // Should work with your alias
|
|
4
4
|
|
|
5
5
|
describe("ConcurrentThrottleBucket Unit Tests", () => {
|
|
6
6
|
it("allows burst capacity immediately", async () => {
|
|
7
|
-
const bucket = new
|
|
7
|
+
const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
|
|
8
8
|
const startTime = Date.now();
|
|
9
9
|
|
|
10
10
|
await Promise.all([
|
|
@@ -18,7 +18,7 @@ describe("ConcurrentThrottleBucket Unit Tests", () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it("delays calls beyond burst at correct rate", async () => {
|
|
21
|
-
const bucket = new
|
|
21
|
+
const bucket = new FlowThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
|
|
22
22
|
const timings: number[] = [];
|
|
23
23
|
const recordTime = async () => {
|
|
24
24
|
await bucket.acquire();
|
|
@@ -44,7 +44,7 @@ describe("ConcurrentThrottleBucket Unit Tests", () => {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it("maintains rate over sustained calls", async () => {
|
|
47
|
-
const bucket = new
|
|
47
|
+
const bucket = new FlowThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
|
|
48
48
|
const timings: number[] = [];
|
|
49
49
|
const recordTime = async () => {
|
|
50
50
|
await bucket.acquire();
|
|
@@ -74,7 +74,7 @@ describe("ConcurrentThrottleBucket Unit Tests", () => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
it("handles high concurrency without exceeding burst", async () => {
|
|
77
|
-
const bucket = new
|
|
77
|
+
const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
|
|
78
78
|
const timings: number[] = [];
|
|
79
79
|
const recordTime = async () => {
|
|
80
80
|
await bucket.acquire();
|