quickbase-js 0.1.0-beta.11 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +40 -18
  3. package/dist/esm/quickbase.d.ts +12 -3
  4. package/dist/esm/quickbase.js +452 -43
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +452 -43
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/package.json +3 -1
  13. package/src/BurstAwareThrottleBucket.ts +57 -0
  14. package/src/FlowThrottleBucket.ts +47 -0
  15. package/src/RateThrottleBucket.ts +5 -0
  16. package/src/Semaphore.ts +33 -0
  17. package/src/authorizationStrategy.ts +12 -1
  18. package/src/generated-unified/QuickbaseClient.ts +1 -1
  19. package/src/invokeMethod.ts +102 -17
  20. package/src/pagination.ts +429 -0
  21. package/src/quickbaseClient.ts +56 -28
  22. package/src/rateLimiter.ts +9 -4
  23. package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
  24. package/tests/vitest/qb/quickbase-js/BurstAwareThrottleBucket.test.ts +125 -0
  25. package/tests/vitest/qb/quickbase-js/FlowBucket.test.ts +77 -0
  26. package/tests/vitest/qb/quickbase-js/paginationSkip.test.ts +111 -0
  27. package/tests/vitest/qb/records/runQuery.test.ts +1 -2
  28. package/tests/vitest/unit/auth/retryUserToken401.test.ts +7 -10
  29. package/tests/vitest/unit/quickbase-js/BurstAwareThrottleBucket.test.ts +108 -0
  30. package/tests/vitest/unit/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +16 -10
  31. package/tests/vitest/unit/quickbase-js/pagination.test.ts +251 -0
  32. package/tests/vitest/unit/{quickbaseClientHttpMethods.test.ts → quickbase-js/quickbaseClientHttpMethods.test.ts} +13 -11
  33. package/tests/vitest/unit/records/runQuery.test.ts +3 -4
  34. package/src/ThrottleBucket.ts +0 -41
  35. package/tests/vitest/unit/umd.test.ts +0 -8
  36. /package/tests/vitest/unit/{rateLimit.test.ts → quickbase-js/rateLimit.test.ts} +0 -0
  37. /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
+ });
@@ -0,0 +1,77 @@
1
+ // tests/vitest/qb/ThrottleBucket.test.ts
2
+ import { describe, it, expect } from "vitest";
3
+ import { quickbase } from "../../../../src/quickbaseClient";
4
+
5
+ describe("QuickbaseClient with ConcurrentThrottleBucket Integration Tests", () => {
6
+ it("handles concurrent API calls with burst and rate limiting in strict order", async () => {
7
+ const mockFetch = async (
8
+ url: string,
9
+ options: RequestInit
10
+ ): Promise<Response> => {
11
+ await new Promise((resolve) => setTimeout(resolve, 500));
12
+ return new Response(JSON.stringify({ id: "mock-app" }), {
13
+ status: 200,
14
+ headers: { "Content-Type": "application/json" },
15
+ });
16
+ };
17
+
18
+ const qbClient = quickbase({
19
+ realm: "test",
20
+ userToken: "mock-token",
21
+ throttle: { rate: 5, burst: 3 },
22
+ fetchApi: mockFetch,
23
+ debug: true,
24
+ });
25
+
26
+ const throttleTimes: number[] = [];
27
+ const startTimes: number[] = new Array(6).fill(0);
28
+ const completionTimes: number[] = [];
29
+ const recordTime = async (index: number) => {
30
+ const preThrottle = Date.now();
31
+ throttleTimes[index] = preThrottle;
32
+ await qbClient.getApp({
33
+ appId: "mock-app-id",
34
+ startTime: startTimes[index],
35
+ });
36
+ completionTimes[index] = Date.now();
37
+ startTimes[index] = startTimes[index] || Date.now() - 500;
38
+ };
39
+
40
+ const startTime = Date.now();
41
+ const promises = [];
42
+ for (let i = 0; i < 6; i++) {
43
+ promises.push(recordTime(i));
44
+ await new Promise((resolve) => setTimeout(resolve, 1));
45
+ }
46
+ await Promise.all(promises);
47
+
48
+ const relativeThrottleTimes = throttleTimes.map((t) => t - startTime);
49
+ const relativeStartTimes = startTimes.map((t) => t - startTime);
50
+ const durations = completionTimes.map((t) => t - startTime);
51
+ const timings = completionTimes.map((t, i) => t - startTimes[i]);
52
+
53
+ console.log("Relative Throttle Times:", relativeThrottleTimes);
54
+ console.log("Relative Start Times (post-throttle):", relativeStartTimes);
55
+ console.log("Durations (completion times):", durations);
56
+ console.log("Timings (fetch durations):", timings);
57
+ console.log("Total Duration:", Date.now() - startTime);
58
+
59
+ const totalDuration = Date.now() - startTime;
60
+ expect(totalDuration).toBeGreaterThanOrEqual(1000);
61
+ expect(totalDuration).toBeLessThan(1200); // Adjusted back to ~1100ms
62
+
63
+ expect(durations.length).toBe(6);
64
+ expect(durations.slice(0, 3).every((d) => d >= 500 && d < 550)).toBe(true);
65
+
66
+ // Sort durations to handle out-of-order completion
67
+ const sortedDurations = [...durations].sort((a, b) => a - b);
68
+ expect(sortedDurations[3]).toBeGreaterThanOrEqual(650);
69
+ expect(sortedDurations[3]).toBeLessThan(750);
70
+ expect(sortedDurations[4]).toBeGreaterThanOrEqual(850);
71
+ expect(sortedDurations[4]).toBeLessThan(950);
72
+ expect(sortedDurations[5]).toBeGreaterThanOrEqual(1050);
73
+ expect(sortedDurations[5]).toBeLessThan(1150);
74
+
75
+ expect(timings.every((t) => t >= 500 && t < 550)).toBe(true);
76
+ });
77
+ });
@@ -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: `QB-USER-TOKEN ${userToken}`,
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: `QB-USER-TOKEN ${userToken}`,
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
- `Retrying getFields with existing user token: b9f3pk_q4j...`
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,23 +1,29 @@
1
+ // test/vitest/unit/ThrottleBucket.test.ts
1
2
  import { describe, it, expect, vi } from "vitest";
2
- import { ThrottleBucket } from "@/ThrottleBucket"; // Adjust path as needed
3
+ import { FlowThrottleBucket } from "@/FlowThrottleBucket"; // Should work with your alias
3
4
 
4
- describe("ThrottleBucket Unit Tests", () => {
5
+ describe("ConcurrentThrottleBucket Unit Tests", () => {
5
6
  it("allows burst capacity immediately", async () => {
6
- const bucket = new ThrottleBucket(5, 3); // rate: 5, burst: 3
7
+ const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
7
8
  const startTime = Date.now();
8
9
 
9
- await Promise.all([bucket.acquire(), bucket.acquire(), bucket.acquire()]);
10
+ await Promise.all([
11
+ bucket.acquire().then(() => bucket.release()),
12
+ bucket.acquire().then(() => bucket.release()),
13
+ bucket.acquire().then(() => bucket.release()),
14
+ ]);
10
15
 
11
16
  const duration = Date.now() - startTime;
12
17
  expect(duration).toBeLessThan(50); // All 3 should be instant (within 50ms)
13
18
  });
14
19
 
15
20
  it("delays calls beyond burst at correct rate", async () => {
16
- const bucket = new ThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
21
+ const bucket = new FlowThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
17
22
  const timings: number[] = [];
18
23
  const recordTime = async () => {
19
24
  await bucket.acquire();
20
25
  timings.push(Date.now());
26
+ bucket.release(); // Release slot
21
27
  };
22
28
 
23
29
  const startTime = Date.now();
@@ -38,11 +44,12 @@ describe("ThrottleBucket Unit Tests", () => {
38
44
  });
39
45
 
40
46
  it("maintains rate over sustained calls", async () => {
41
- const bucket = new ThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
47
+ const bucket = new FlowThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
42
48
  const timings: number[] = [];
43
49
  const recordTime = async () => {
44
50
  await bucket.acquire();
45
51
  timings.push(Date.now());
52
+ bucket.release(); // Release slot
46
53
  };
47
54
 
48
55
  const startTime = Date.now();
@@ -67,11 +74,12 @@ describe("ThrottleBucket Unit Tests", () => {
67
74
  });
68
75
 
69
76
  it("handles high concurrency without exceeding burst", async () => {
70
- const bucket = new ThrottleBucket(5, 3); // rate: 5, burst: 3
77
+ const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
71
78
  const timings: number[] = [];
72
79
  const recordTime = async () => {
73
80
  await bucket.acquire();
74
81
  timings.push(Date.now());
82
+ bucket.release(); // Release slot
75
83
  };
76
84
 
77
85
  const startTime = Date.now();
@@ -83,9 +91,7 @@ describe("ThrottleBucket Unit Tests", () => {
83
91
 
84
92
  const durations = timings.map((t) => t - startTime);
85
93
  expect(durations.length).toBe(10);
86
- // First 3 should be instant
87
- expect(durations.slice(0, 3).every((d) => d < 50)).toBe(true);
88
- // Next calls should be staggered at ~200ms intervals
94
+ expect(durations.slice(0, 3).every((d) => d < 50)).toBe(true); // First 3 instant
89
95
  for (let i = 3; i < 10; i++) {
90
96
  const expectedDelay = (i - 2) * 200; // After burst, 200ms per token
91
97
  expect(durations[i]).toBeGreaterThanOrEqual(expectedDelay - 50);