quickbase-js 0.1.0-beta.10 → 0.1.0-beta.12

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 (35) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +56 -24
  3. package/dist/esm/quickbase.d.ts +1 -0
  4. package/dist/esm/quickbase.js +125 -87
  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 +125 -87
  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/examples/quicbase-jsCDN.html +2 -2
  13. package/package.json +1 -1
  14. package/src/ThrottleBucket.ts +52 -16
  15. package/src/authorizationStrategy.ts +62 -35
  16. package/src/generated-unified/QuickbaseClient.ts +1 -1
  17. package/src/invokeMethod.ts +33 -66
  18. package/src/quickbaseClient.ts +12 -7
  19. package/src/rateLimiter.ts +8 -3
  20. package/src/tokenCache.ts +4 -0
  21. package/tests/playwright/qb/auth/getAppWithTempTokenConcurrency.test.ts +208 -0
  22. package/tests/vitest/qb/ThrottleBucket.test.ts +77 -0
  23. package/tests/vitest/qb/auth/ssoTokenConcurrency.test.ts +68 -0
  24. package/tests/vitest/qb/auth/userTokenConcurrency.test.ts +63 -0
  25. package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
  26. package/tests/vitest/unit/ThrottleBucket.test.ts +16 -10
  27. package/tests/vitest/unit/apps/getAppConcurrency.test.ts +151 -0
  28. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +20 -2
  29. package/tests/vitest/unit/auth/userTokenConcurrency.test.ts +62 -0
  30. package/tests/vitest/unit/index.ts +1 -1
  31. package/tests/vitest/unit/quickbaseClientHttpMethods.test.ts +3 -3
  32. package/tools/log-gen.ts +1 -1
  33. package/tools/log-gen.yaml +130 -78
  34. package/api-examples.md +0 -49
  35. package/markdown-examples.md +0 -85
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [0.1.0-beta.12](https://github.com/DrewBradfordXYZ/quickbase-js/compare/v0.1.0-beta.11...v0.1.0-beta.12) (2025-03-23)
6
+
7
+
8
+ ### Features
9
+
10
+ * enable true concurrency with throttle bucket ([8dc4dee](https://github.com/DrewBradfordXYZ/quickbase-js/commit/8dc4deeaddb4365f4f64a8bd07390c7071f739f7))
11
+
12
+ ## [0.1.0-beta.11](https://github.com/DrewBradfordXYZ/quickbase-js/compare/v0.1.0-beta.10...v0.1.0-beta.11) (2025-03-23)
13
+
14
+
15
+ ### Features
16
+
17
+ * only fetch auth dbid once for concurrent requests. temp tokens and SSO ([12f7f31](https://github.com/DrewBradfordXYZ/quickbase-js/commit/12f7f3117b2fd6e5db35d73e945dff0a79ef0481))
18
+ * SSO concurrency tests passes ([caf5fca](https://github.com/DrewBradfordXYZ/quickbase-js/commit/caf5fca4e86b0dda4a75c2856f9324634369f15a))
19
+ * temp token concurrency prevents redundant token fetches. getAppConcurrency.test.ts unit test passes ([2913e7b](https://github.com/DrewBradfordXYZ/quickbase-js/commit/2913e7b38d38ca0ceaa0af21b2e872f7bafb9d99))
20
+
5
21
  ## [0.1.0-beta.10](https://github.com/DrewBradfordXYZ/quickbase-js/compare/v0.1.0-beta.9...v0.1.0-beta.10) (2025-03-22)
6
22
 
7
23
 
package/README.md CHANGED
@@ -1,22 +1,60 @@
1
1
  # quickbase-js
2
2
 
3
- A Typescript library using the QuickBase OpenAPI spec and the typescipt-fetch generator. This library is designed for both browser and Node.js environments. Code pages have first class support.
3
+ A Typescript API client for the [Quickbase JSON RESTful API](https://developer.quickbase.com/).
4
4
 
5
- Authentication methods: `Temporary Tokens`, `User Tokens`, `SSO`.
5
+ Support for various authentication strategies (`user token`, `temporary tokens`, `SSO token`), `rate limiting`, `429`(rate limit) error and `401`(auth) error handling retries and automatic `date` conversion. It uses a Proxy to dynamically invoke API methods derived from the OpenAPI specification.
6
+
7
+ Browser and Node.js support. ESM and UMD builds are available.
8
+
9
+ Authentication methods are handled in an opinionated manner. See configuration options for details and available settings.
6
10
 
7
11
  ## API Documentation
8
12
 
9
- https://quickbase-js.netlify.app/
13
+ <a href="https://quickbase-js.netlify.app/" target="_blank"> Documentation</a> for quickbase-js.
10
14
 
11
15
  ## Installation
12
16
 
17
+ #### Node.js install
18
+
13
19
  ```bash
14
20
  npm install --save quickbase-js
15
21
  ```
16
22
 
17
- ## Examples
23
+ #### CDN install.
24
+
25
+ Find and select the latest version. UMD is recommended for code pages.
26
+
27
+ <a href="https://www.jsdelivr.com/package/npm/quickbase-js?tab=files" target="_blank">CDN Files</a> for quickbase-js
28
+
29
+ ```html
30
+ <!DOCTYPE html>
31
+ <html>
32
+ <head>
33
+ <title>Quickbase</title>
34
+ <!-- Replace xx.xx.xx with the latest version from CDN Files-->
35
+ <script src="https://cdn.jsdelivr.net/npm/quickbase-js@xx.xx.xx/dist/umd/quickbase.umd.min.js"></script>
36
+ </head>
37
+ <body>
38
+ <script>
39
+ const realm = ""; // Replace with your realm
40
+ const appId = ""; // Replace with your app ID
41
+
42
+ const qb = QuickbaseJS.quickbase({
43
+ realm,
44
+ useTempTokens: true,
45
+ });
46
+
47
+ qb.getApp({ appId })
48
+ .then((app) => console.log(app.name))
49
+ .catch((err) => console.log(err.message));
50
+ </script>
51
+ </body>
52
+ </html>
53
+ ```
54
+
55
+ ## Authentication Examples
18
56
 
19
- ### Auth: User Token
57
+ ### User Token
20
58
 
21
59
  ```javascript
22
60
  import { quickbase } from "quickbase-js"; // Adjust the import path as needed
@@ -45,7 +83,7 @@ const getAppDetails = async () => {
45
83
  getAppDetails();
46
84
  ```
47
85
 
48
- ### Auth: Temporary Tokens (Code Pages)
86
+ ### Temporary Tokens (Code Pages)
49
87
 
50
88
  ```javascript
51
89
  import { quickbase } from "quickbase-js"; // Adjust the import path as needed
@@ -73,7 +111,7 @@ const getAppDetails = async () => {
73
111
  getAppDetails();
74
112
  ```
75
113
 
76
- ### Auth: SSO
114
+ ### SSO Token
77
115
 
78
116
  ```javascript
79
117
  import { quickbase } from "quickbase-js"; // Adjust the import path as needed
@@ -117,37 +155,31 @@ _User options:_
117
155
 
118
156
  _Advanced user options:_
119
157
 
120
- - **`tempTokenLifespan`** (`number`, optional): The lifespan (in milliseconds) of temporary tokens in the cache. Defaults to 4 minutes 50 seconds (290000 ms).
121
-
122
- - **`throttle`** (`{ rate: number; burst: number }`, optional): Configures rate limiting, where `rate` is requests per second and `burst` is the maximum burst of requests. Defaults to `{ rate: 10, burst: 10 }`.
123
-
158
+ - **`throttle`** (`{ rate: number; burst: number }`, optional): Configures rate limiting to manage how many API requests are sent at once and how quickly additional requests follow. Defaults to `{ rate: 5, burst: 3 }`.
159
+ - **`burst`**: The number of requests that can start immediately (concurrent calls). Default is `3`, meaning up to 3 requests run at the same time.
160
+ - **`rate`**: The number of requests allowed per second after the initial burst. Default is `5`, pacing extra requests at one every ~200ms.
161
+ - **How It Works**: Taking the default setting for example: requests in general take ~500ms to finish. Imagine having 6 requests, 3 start right away finishing at ~500ms. Because the rate is set to 200ms, 2 request tokens have built up during this time, so the next 2 requests are sent out immediately when the first 3 finnish, leaving 1 burst spot available. The third request waits the extra 100ms to be dispatched starting at ~600ms using up the remaining burst spot, which takes another ~500ms to finish. All requests finishing around ~1100ms total. This keeps you safely under Quickbase’s 10 requests/sec limit while maximizing speed. A more liberal setting of `{ rate: 10, burst: 5 }` allows 5 requests at once, with request tokens building up every ~100ms, finishing 6 requests in ~1000ms.
124
162
  - **`maxRetries`** (`number`, optional): The maximum number of retries for failed requests. Defaults to `3`.
125
-
126
163
  - **`retryDelay`** (`number`, optional): The base delay (in milliseconds) between retries, which increases exponentially. Defaults to `1000`.
127
-
164
+ - **`tempTokenLifespan`** (`number`, optional): The lifespan (in milliseconds) of temporary tokens in the cache. Defaults to 4 minutes 50 seconds (290000 ms).
128
165
  - **`convertDates`** (`boolean`, optional): Converts ISO date strings to `Date` objects in responses. Defaults to `true`.
129
166
 
130
167
  _Overrides and development options:_
131
168
 
132
- - **`fetchApi`** (`typeof fetch`, optional): For browser environments, this defaults to the built-in `fetch`. In Node.js, you are able to use `node-fetch` or another compatible library.
133
-
169
+ - **`fetchApi`** (`typeof fetch`, optional): For browser environments, this defaults to the built-in `fetch`. In Node.js, you can use `node-fetch` or another compatible library.
134
170
  - **`baseUrl`** (`string`, optional): The base URL for the QuickBase API. Defaults to `"https://api.quickbase.com/v1"`.
135
-
136
171
  - **`debug`** (`boolean`, optional): Enables debug logging to the console. Defaults to `false`.
137
-
138
- - **`tempToken`** (`string`, optional): If you would like to override the default behavior of tempTokens you can provide your own tempToken.
139
-
140
- - **`tokenCache`** (`TokenCache`, optional): If you would like to use your own token cache, you can provide an instance of `TokenCache`.
172
+ - **`tempToken`** (`string`, optional): Overrides the default behavior of tempTokens by providing your own tempToken.
173
+ - **`tokenCache`** (`TokenCache`, optional): Allows you to use your own token cache instance.
141
174
 
142
175
  ---
143
176
 
144
177
  #### Configuration Notes
145
178
 
146
- - **Authentication**: There are three mutually exclusive authentication methods: `userToken`, `useTempTokens` and `useSso` with `samlToken`. Only one auth method may be active at a time.
179
+ - **Authentication**: There are three mutually exclusive authentication methods: `userToken`, `useTempTokens`, and `useSso` with `samlToken`. Only one auth method may be active at a time.
180
+ - **`tokenCache`**: Temporary tokens are cached with their dbid and `tempTokenLifespan`. This is automatically set up when `useTempTokens` is enabled.
147
181
 
148
- - **`throttle`**: Adjust this to handle 429 Too Many Requests errors. Controls API request pacing to avoid hitting rate limits. For example, `{ rate: 5, burst: 20 }` allows 5 requests per second with a burst capacity of 20.
149
-
150
- - **`tokenCache`**: Temporary tokens are cached with their dbid with a `tempTokenLifespan`. This is automatically setup for you when `useTempTokens` is enabled.
182
+ ---
151
183
 
152
184
  ### Development workflow
153
185
 
@@ -8051,6 +8051,7 @@ declare class TokenCache {
8051
8051
  constructor(tempTokenLifespan?: number);
8052
8052
  get(dbid: string): string | undefined;
8053
8053
  set(dbid: string, token: string, lifespan?: number): void;
8054
+ delete(dbid: string): void;
8054
8055
  clear(): void;
8055
8056
  }
8056
8057
 
@@ -7348,6 +7348,9 @@ class TokenCache {
7348
7348
  expiresAt: now + (lifespan || this.tempTokenLifespan),
7349
7349
  });
7350
7350
  }
7351
+ delete(dbid) {
7352
+ this.cache.delete(dbid);
7353
+ }
7351
7354
  clear() {
7352
7355
  this.cache.clear();
7353
7356
  }
@@ -7402,14 +7405,13 @@ function extractHttpMethod(method) {
7402
7405
  return match ? match[1].toUpperCase() : "GET"; // Fallback to GET if no match
7403
7406
  }
7404
7407
 
7405
- // src/authorizationStrategy.ts
7406
- // src/authorizationStrategy.ts (partial update)
7407
7408
  class TempTokenStrategy {
7408
7409
  tokenCache;
7409
7410
  initialTempToken;
7410
7411
  fetchApi;
7411
7412
  realm;
7412
7413
  baseUrl;
7414
+ pendingFetches = new Map();
7413
7415
  constructor(tokenCache, initialTempToken, fetchApi, realm, baseUrl = "https://api.quickbase.com/v1") {
7414
7416
  this.tokenCache = tokenCache;
7415
7417
  this.initialTempToken = initialTempToken;
@@ -7444,7 +7446,13 @@ class TempTokenStrategy {
7444
7446
  async getToken(dbid) {
7445
7447
  let token = this.tokenCache.get(dbid) || this.initialTempToken;
7446
7448
  if (!token && dbid) {
7447
- token = await this.fetchTempToken(dbid);
7449
+ if (this.pendingFetches.has(dbid)) {
7450
+ console.log(`[getToken] Waiting for existing fetch for dbid: ${dbid}`);
7451
+ return this.pendingFetches.get(dbid);
7452
+ }
7453
+ const fetchPromise = this.fetchTempToken(dbid).finally(() => this.pendingFetches.delete(dbid));
7454
+ this.pendingFetches.set(dbid, fetchPromise);
7455
+ token = await fetchPromise;
7448
7456
  }
7449
7457
  return token;
7450
7458
  }
@@ -7464,15 +7472,29 @@ class TempTokenStrategy {
7464
7472
  }
7465
7473
  if (debug)
7466
7474
  console.log(`Refreshing temp token for dbid: ${dbid}`);
7467
- const newToken = await this.fetchTempToken(dbid);
7468
- this.tokenCache.set(dbid, newToken);
7469
- return newToken;
7475
+ // Invalidate the cache for this dbid to force a fresh fetch
7476
+ this.tokenCache.delete(dbid);
7477
+ try {
7478
+ const newToken = await this.getToken(dbid);
7479
+ if (newToken) {
7480
+ this.tokenCache.set(dbid, newToken);
7481
+ return newToken;
7482
+ }
7483
+ return null;
7484
+ }
7485
+ catch (error) {
7486
+ if (debug)
7487
+ console.log(`[${methodName || "method"}] Failed to refresh token:`, error);
7488
+ throw error;
7489
+ }
7470
7490
  }
7471
7491
  }
7472
7492
  class UserTokenStrategy {
7473
7493
  userToken;
7474
- constructor(userToken) {
7494
+ baseUrl;
7495
+ constructor(userToken, baseUrl = "https://api.quickbase.com/v1") {
7475
7496
  this.userToken = userToken;
7497
+ this.baseUrl = baseUrl;
7476
7498
  }
7477
7499
  async getToken(_dbid) {
7478
7500
  return this.userToken;
@@ -7493,21 +7515,31 @@ class SsoTokenStrategy {
7493
7515
  realm;
7494
7516
  fetchApi;
7495
7517
  debug;
7518
+ baseUrl;
7496
7519
  currentToken;
7497
- constructor(samlToken, realm, fetchApi, debug = false) {
7520
+ pendingFetches = new Map();
7521
+ constructor(samlToken, realm, fetchApi, debug = false, baseUrl = "https://api.quickbase.com/v1") {
7498
7522
  this.samlToken = samlToken;
7499
7523
  this.realm = realm;
7500
7524
  this.fetchApi = fetchApi;
7501
7525
  this.debug = debug;
7526
+ this.baseUrl = baseUrl;
7502
7527
  }
7503
7528
  async getToken(_dbid) {
7504
7529
  if (!this.currentToken) {
7505
- this.currentToken = await this.fetchSsoToken({
7530
+ if (this.pendingFetches.has("sso")) {
7531
+ if (this.debug)
7532
+ console.log("[getToken] Waiting for existing SSO fetch");
7533
+ return this.pendingFetches.get("sso");
7534
+ }
7535
+ const fetchPromise = this.fetchSsoToken({
7506
7536
  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
7507
7537
  requested_token_type: "urn:quickbase:params:oauth:token-type:temp_token",
7508
7538
  subject_token: this.samlToken,
7509
7539
  subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
7510
- });
7540
+ }).finally(() => this.pendingFetches.delete("sso"));
7541
+ this.pendingFetches.set("sso", fetchPromise);
7542
+ this.currentToken = await fetchPromise;
7511
7543
  }
7512
7544
  return this.currentToken;
7513
7545
  }
@@ -7534,7 +7566,7 @@ class SsoTokenStrategy {
7534
7566
  subject_token: this.samlToken,
7535
7567
  subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
7536
7568
  };
7537
- const response = await this.fetchApi(`https://api.quickbase.com/v1/auth/oauth/token`, {
7569
+ const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
7538
7570
  method: "POST",
7539
7571
  headers: {
7540
7572
  "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
@@ -7561,7 +7593,7 @@ class SsoTokenStrategy {
7561
7593
  return newToken;
7562
7594
  }
7563
7595
  async fetchSsoToken(params) {
7564
- const response = await this.fetchApi(`https://api.quickbase.com/v1/auth/oauth/token`, {
7596
+ const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
7565
7597
  method: "POST",
7566
7598
  headers: {
7567
7599
  "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
@@ -7609,16 +7641,15 @@ class RateLimitError extends Error {
7609
7641
  }
7610
7642
 
7611
7643
  // src/invokeMethod.ts
7612
- async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates) {
7613
- console.log("[invokeMethod] Starting for method:", methodName);
7644
+ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, attempt = 0, maxAttempts = rateLimiter.maxRetries + 1) {
7614
7645
  const methodInfo = methodMap[methodName];
7615
7646
  if (!methodInfo)
7616
7647
  throw new Error(`Method ${methodName} not found`);
7617
7648
  const hasBody = "body" in params && params.body !== undefined;
7618
7649
  const body = hasBody ? params.body : undefined;
7619
7650
  const restParams = hasBody
7620
- ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body"))
7621
- : { ...params };
7651
+ ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body" && key !== "startTime"))
7652
+ : { ...params, startTime: undefined };
7622
7653
  const requestParameters = {
7623
7654
  ...restParams,
7624
7655
  ...(hasBody ? { generated: body } : {}),
@@ -7628,33 +7659,21 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7628
7659
  method: methodInfo.httpMethod,
7629
7660
  };
7630
7661
  let dbid = extractDbid(params);
7631
- console.log("[invokeMethod] Extracted dbid:", dbid);
7632
7662
  let token = dbid
7633
7663
  ? await authStrategy.getToken(dbid)
7634
7664
  : await authStrategy.getToken("");
7635
- console.log("[invokeMethod] Initial token:", token);
7636
7665
  if (token) {
7637
7666
  authStrategy.applyHeaders(baseHeaders, token);
7638
7667
  requestOptions.headers = { ...baseHeaders };
7639
- console.log("[invokeMethod] Token found, headers set:", requestOptions.headers);
7640
7668
  if (methodName === "getTempTokenDBID") {
7641
- console.log("[invokeMethod] Using cached token:", token);
7642
7669
  return { temporaryAuthorization: token };
7643
7670
  }
7644
7671
  }
7645
- if (debug) {
7646
- console.log(`[${methodName}] requestParameters:`, requestParameters);
7647
- console.log(`[${methodName}] requestOptions:`, requestOptions);
7648
- }
7649
7672
  async function processResponse(rawResponse) {
7650
- if (debug)
7651
- console.log(`[${methodName}] rawResponse:`, rawResponse);
7652
7673
  if (rawResponse instanceof Response) {
7653
7674
  const contentType = rawResponse.headers
7654
7675
  .get("Content-Type")
7655
7676
  ?.toLowerCase();
7656
- if (debug)
7657
- console.log(`[${methodName}] contentType:`, contentType);
7658
7677
  if (contentType?.includes("application/json")) {
7659
7678
  const jsonResponse = await rawResponse.json();
7660
7679
  return transformDates(jsonResponse, convertDates);
@@ -7663,8 +7682,6 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7663
7682
  }
7664
7683
  if (rawResponse && typeof rawResponse.value === "function") {
7665
7684
  const response = await rawResponse.value();
7666
- if (debug)
7667
- console.log(`[${methodName}] Resolved JSONApiResponse:`, response);
7668
7685
  return transformDates(response, convertDates);
7669
7686
  }
7670
7687
  return transformDates(rawResponse, convertDates);
@@ -7692,28 +7709,26 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7692
7709
  }
7693
7710
  }
7694
7711
  catch (e) {
7695
- if (debug)
7696
- console.log(`[${methodName}] Error parsing response body:`, e);
7697
7712
  message = "Failed to parse error response";
7698
7713
  }
7699
- if (debug)
7700
- console.log("[invokeMethod] Parsed error - status:", status, "message:", message);
7701
7714
  return { message, status };
7702
7715
  }
7703
- let attempt = 0;
7704
- const maxAttempts = rateLimiter.maxRetries + 1;
7705
7716
  while (attempt < maxAttempts) {
7706
- console.log("[invokeMethod] Attempt:", attempt + 1, "of", maxAttempts);
7717
+ let acquired = false;
7707
7718
  try {
7708
7719
  await rateLimiter.throttle();
7709
- console.log("[invokeMethod] About to call API for method:", methodName);
7710
- const response = await methodInfo.method(requestParameters, requestOptions);
7711
- console.log("[invokeMethod] API call completed for method:", methodName);
7720
+ acquired = true;
7721
+ const postThrottleTime = Date.now(); // Capture post-throttle time
7722
+ if (params.startTime !== undefined)
7723
+ params.startTime = postThrottleTime; // Update startTime for testing
7724
+ const responsePromise = methodInfo.method(requestParameters, requestOptions);
7725
+ rateLimiter.release(); // Release slot immediately after starting fetch
7726
+ const response = await responsePromise;
7712
7727
  return await processResponse(response);
7713
7728
  }
7714
7729
  catch (error) {
7715
- if (debug)
7716
- console.log("[invokeMethod] Caught error:", error);
7730
+ if (acquired)
7731
+ rateLimiter.release();
7717
7732
  let status;
7718
7733
  let message;
7719
7734
  let response;
@@ -7726,31 +7741,13 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7726
7741
  ({ message, status } = await parseErrorResponse(response));
7727
7742
  }
7728
7743
  else {
7729
- if (debug)
7730
- console.log(`[${methodName}] Unexpected error:`, error);
7731
- // Attempt to handle as a fetch error with a response
7732
- if (error instanceof Error && "response" in error) {
7733
- response = error.response;
7734
- if (response instanceof Response) {
7735
- ({ message, status } = await parseErrorResponse(response));
7736
- }
7737
- else {
7738
- throw error; // Rethrow if no response
7739
- }
7740
- }
7741
- else {
7742
- throw error; // Rethrow truly unexpected errors
7743
- }
7744
+ throw error;
7744
7745
  }
7745
- if (debug)
7746
- console.log("[invokeMethod] Handling error with status:", status);
7747
7746
  if (status === 429) {
7748
7747
  if (!(error instanceof ResponseError)) {
7749
7748
  throw new Error("Expected ResponseError for 429 handling");
7750
7749
  }
7751
7750
  const delay = await rateLimiter.handle429(error, attempt + 1);
7752
- if (debug)
7753
- console.log(`[${methodName}] 429 delay: ${delay}ms`);
7754
7751
  if (attempt + 1 === maxAttempts) {
7755
7752
  throw new RateLimitError(`API Error: ${message} (Status: ${status})`, status, response?.headers.get("Retry-After")
7756
7753
  ? parseInt(response.headers.get("Retry-After"), 10)
@@ -7760,14 +7757,20 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7760
7757
  attempt++;
7761
7758
  continue;
7762
7759
  }
7763
- const newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
7764
- if (newToken) {
7760
+ let newToken;
7761
+ try {
7762
+ newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
7763
+ }
7764
+ catch (authError) {
7765
+ throw authError;
7766
+ }
7767
+ if (newToken !== null) {
7765
7768
  token = newToken;
7766
7769
  authStrategy.applyHeaders(baseHeaders, token);
7767
7770
  requestOptions.headers = { ...baseHeaders };
7768
- attempt++;
7769
7771
  if (debug)
7770
7772
  console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
7773
+ attempt++;
7771
7774
  continue;
7772
7775
  }
7773
7776
  throw new Error(`API Error: ${message} (Status: ${status})`);
@@ -7777,41 +7780,71 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7777
7780
  }
7778
7781
 
7779
7782
  // src/ThrottleBucket.ts
7780
- class ThrottleBucket {
7783
+ class ConcurrentThrottleBucket {
7781
7784
  tokens;
7782
7785
  maxTokens;
7783
- refillRate; // Tokens per second
7786
+ refillRate;
7784
7787
  lastRefill;
7785
- pending = Promise.resolve(); // Queue for sequential execution
7788
+ semaphore;
7786
7789
  constructor(rate, burst) {
7787
7790
  this.tokens = burst;
7788
7791
  this.maxTokens = burst;
7789
7792
  this.refillRate = rate;
7790
7793
  this.lastRefill = Date.now();
7794
+ this.semaphore = new Semaphore(burst);
7791
7795
  }
7792
7796
  refill() {
7793
7797
  const now = Date.now();
7794
- const elapsed = (now - this.lastRefill) / 1000; // Seconds elapsed
7798
+ const elapsed = (now - this.lastRefill) / 1000;
7795
7799
  const newTokens = elapsed * this.refillRate;
7796
7800
  this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
7797
7801
  this.lastRefill = now;
7798
7802
  }
7799
7803
  async acquire() {
7800
- // Chain the new acquisition onto the pending queue
7801
- const previous = this.pending;
7802
- this.pending = (async () => {
7803
- await previous; // Wait for prior calls to complete
7804
+ await this.semaphore.acquire();
7805
+ try {
7804
7806
  this.refill();
7805
- if (this.tokens >= 1) {
7806
- this.tokens -= 1;
7807
- return;
7807
+ while (this.tokens < 1) {
7808
+ const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
7809
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
7810
+ this.refill();
7808
7811
  }
7809
- const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
7810
- await new Promise((resolve) => setTimeout(resolve, waitTime));
7811
- this.refill();
7812
7812
  this.tokens -= 1;
7813
- })();
7814
- await this.pending;
7813
+ }
7814
+ catch (error) {
7815
+ this.semaphore.release();
7816
+ throw error;
7817
+ }
7818
+ }
7819
+ release() {
7820
+ this.semaphore.release();
7821
+ }
7822
+ }
7823
+ class Semaphore {
7824
+ permits;
7825
+ waiting = [];
7826
+ constructor(maxPermits) {
7827
+ this.permits = maxPermits;
7828
+ }
7829
+ async acquire() {
7830
+ if (this.permits > 0) {
7831
+ this.permits -= 1;
7832
+ return;
7833
+ }
7834
+ return new Promise((resolve, reject) => {
7835
+ this.waiting.push({ resolve, reject });
7836
+ });
7837
+ }
7838
+ release() {
7839
+ this.permits += 1;
7840
+ if (this.waiting.length > 0 && this.permits > 0) {
7841
+ const { resolve } = this.waiting.shift();
7842
+ this.permits -= 1;
7843
+ resolve();
7844
+ }
7845
+ }
7846
+ available() {
7847
+ return this.permits;
7815
7848
  }
7816
7849
  }
7817
7850
 
@@ -7819,7 +7852,8 @@ class RateLimiter {
7819
7852
  throttleBucket;
7820
7853
  maxRetries;
7821
7854
  retryDelay;
7822
- constructor(throttleBucket, maxRetries = 3, retryDelay = 1000) {
7855
+ constructor(throttleBucket, // Updated type
7856
+ maxRetries = 3, retryDelay = 1000) {
7823
7857
  this.throttleBucket = throttleBucket;
7824
7858
  this.maxRetries = maxRetries;
7825
7859
  this.retryDelay = retryDelay;
@@ -7829,8 +7863,12 @@ class RateLimiter {
7829
7863
  await this.throttleBucket.acquire();
7830
7864
  }
7831
7865
  }
7866
+ release() {
7867
+ if (this.throttleBucket) {
7868
+ this.throttleBucket.release();
7869
+ }
7870
+ }
7832
7871
  async handle429(error, attempt) {
7833
- // Remove the throw here; let invokeMethod handle exhaustion
7834
7872
  const retryAfter = error.response.headers.get("Retry-After");
7835
7873
  return retryAfter
7836
7874
  ? parseInt(retryAfter, 10) * 1000
@@ -7838,12 +7876,12 @@ class RateLimiter {
7838
7876
  }
7839
7877
  }
7840
7878
 
7841
- // src/quickbaseClient.ts
7842
7879
  function quickbase(config) {
7843
- const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate: 10, burst: 10 }, maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", } = config;
7880
+ const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate: 5, burst: 3 }, // Updated default to match passing test
7881
+ maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", } = config;
7844
7882
  const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
7845
7883
  const throttleBucket = throttle
7846
- ? new ThrottleBucket(throttle.rate, throttle.burst)
7884
+ ? new ConcurrentThrottleBucket(throttle.rate, throttle.burst)
7847
7885
  : null;
7848
7886
  const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
7849
7887
  const defaultFetch = typeof globalThis.window !== "undefined"
@@ -7854,10 +7892,10 @@ function quickbase(config) {
7854
7892
  throw new Error("No fetch implementation available");
7855
7893
  }
7856
7894
  const authStrategy = useSso
7857
- ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
7895
+ ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
7858
7896
  : useTempTokens
7859
7897
  ? new TempTokenStrategy(tokenCache, tempToken, effectiveFetch, realm, baseUrl)
7860
- : new UserTokenStrategy(userToken || "");
7898
+ : new UserTokenStrategy(userToken || "", baseUrl);
7861
7899
  const baseHeaders = {
7862
7900
  "QB-Realm-Hostname": `${realm}.quickbase.com`,
7863
7901
  "Content-Type": "application/json",