quickbase-js 0.1.0-beta.11 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +10 -18
- package/dist/esm/quickbase.js +70 -28
- 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 +70 -28
- 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 +1 -1
- package/src/ThrottleBucket.ts +52 -16
- package/src/generated-unified/QuickbaseClient.ts +1 -1
- package/src/invokeMethod.ts +15 -8
- package/src/quickbaseClient.ts +3 -3
- package/src/rateLimiter.ts +8 -3
- package/tests/vitest/qb/ThrottleBucket.test.ts +77 -0
- package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
- package/tests/vitest/unit/ThrottleBucket.test.ts +16 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
|
|
5
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)
|
|
6
13
|
|
|
7
14
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Typescript API client for the [Quickbase JSON RESTful API](https://developer.quickbase.com/).
|
|
4
4
|
|
|
5
|
-
Support for various authentication strategies (`user token`, `temporary tokens`, `SSO token`), `rate limiting`, `429` and `401` error handling retries and automatic `date` conversion. It uses a Proxy to dynamically invoke API methods derived from the OpenAPI specification.
|
|
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
6
|
|
|
7
7
|
Browser and Node.js support. ESM and UMD builds are available.
|
|
8
8
|
|
|
@@ -155,37 +155,29 @@ _User options:_
|
|
|
155
155
|
|
|
156
156
|
_Advanced user options:_
|
|
157
157
|
|
|
158
|
-
- **`throttle`** (`{ rate: number; burst: number }`, optional): Configures rate limiting
|
|
159
|
-
|
|
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.
|
|
160
162
|
- **`maxRetries`** (`number`, optional): The maximum number of retries for failed requests. Defaults to `3`.
|
|
161
|
-
|
|
162
163
|
- **`retryDelay`** (`number`, optional): The base delay (in milliseconds) between retries, which increases exponentially. Defaults to `1000`.
|
|
163
|
-
|
|
164
164
|
- **`tempTokenLifespan`** (`number`, optional): The lifespan (in milliseconds) of temporary tokens in the cache. Defaults to 4 minutes 50 seconds (290000 ms).
|
|
165
|
-
|
|
166
165
|
- **`convertDates`** (`boolean`, optional): Converts ISO date strings to `Date` objects in responses. Defaults to `true`.
|
|
167
166
|
|
|
168
167
|
_Overrides and development options:_
|
|
169
168
|
|
|
170
|
-
- **`fetchApi`** (`typeof fetch`, optional): For browser environments, this defaults to the built-in `fetch`. In Node.js, you
|
|
171
|
-
|
|
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.
|
|
172
170
|
- **`baseUrl`** (`string`, optional): The base URL for the QuickBase API. Defaults to `"https://api.quickbase.com/v1"`.
|
|
173
|
-
|
|
174
171
|
- **`debug`** (`boolean`, optional): Enables debug logging to the console. Defaults to `false`.
|
|
175
|
-
|
|
176
|
-
- **`
|
|
177
|
-
|
|
178
|
-
- **`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.
|
|
179
174
|
|
|
180
175
|
---
|
|
181
176
|
|
|
182
177
|
#### Configuration Notes
|
|
183
178
|
|
|
184
|
-
- **Authentication**: There are three mutually exclusive authentication methods: `userToken`, `useTempTokens
|
|
185
|
-
|
|
186
|
-
- **`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.
|
|
187
|
-
|
|
188
|
-
- **`tokenCache`**: Temporary tokens are cached with their dbid with a `tempTokenLifespan`. This is automatically setup for you when `useTempTokens` is enabled.
|
|
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.
|
|
189
181
|
|
|
190
182
|
---
|
|
191
183
|
|
package/dist/esm/quickbase.js
CHANGED
|
@@ -7648,8 +7648,8 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7648
7648
|
const hasBody = "body" in params && params.body !== undefined;
|
|
7649
7649
|
const body = hasBody ? params.body : undefined;
|
|
7650
7650
|
const restParams = hasBody
|
|
7651
|
-
? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body"))
|
|
7652
|
-
: { ...params };
|
|
7651
|
+
? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body" && key !== "startTime"))
|
|
7652
|
+
: { ...params, startTime: undefined };
|
|
7653
7653
|
const requestParameters = {
|
|
7654
7654
|
...restParams,
|
|
7655
7655
|
...(hasBody ? { generated: body } : {}),
|
|
@@ -7714,12 +7714,21 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7714
7714
|
return { message, status };
|
|
7715
7715
|
}
|
|
7716
7716
|
while (attempt < maxAttempts) {
|
|
7717
|
+
let acquired = false;
|
|
7717
7718
|
try {
|
|
7718
7719
|
await rateLimiter.throttle();
|
|
7719
|
-
|
|
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;
|
|
7720
7727
|
return await processResponse(response);
|
|
7721
7728
|
}
|
|
7722
7729
|
catch (error) {
|
|
7730
|
+
if (acquired)
|
|
7731
|
+
rateLimiter.release();
|
|
7723
7732
|
let status;
|
|
7724
7733
|
let message;
|
|
7725
7734
|
let response;
|
|
@@ -7732,7 +7741,7 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7732
7741
|
({ message, status } = await parseErrorResponse(response));
|
|
7733
7742
|
}
|
|
7734
7743
|
else {
|
|
7735
|
-
throw error;
|
|
7744
|
+
throw error;
|
|
7736
7745
|
}
|
|
7737
7746
|
if (status === 429) {
|
|
7738
7747
|
if (!(error instanceof ResponseError)) {
|
|
@@ -7748,15 +7757,13 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7748
7757
|
attempt++;
|
|
7749
7758
|
continue;
|
|
7750
7759
|
}
|
|
7751
|
-
// Handle authentication errors
|
|
7752
7760
|
let newToken;
|
|
7753
7761
|
try {
|
|
7754
7762
|
newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
|
|
7755
7763
|
}
|
|
7756
7764
|
catch (authError) {
|
|
7757
|
-
throw authError;
|
|
7765
|
+
throw authError;
|
|
7758
7766
|
}
|
|
7759
|
-
// Only proceed with retry if we have a valid new token
|
|
7760
7767
|
if (newToken !== null) {
|
|
7761
7768
|
token = newToken;
|
|
7762
7769
|
authStrategy.applyHeaders(baseHeaders, token);
|
|
@@ -7766,7 +7773,6 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7766
7773
|
attempt++;
|
|
7767
7774
|
continue;
|
|
7768
7775
|
}
|
|
7769
|
-
// If no new token was provided, throw the original error
|
|
7770
7776
|
throw new Error(`API Error: ${message} (Status: ${status})`);
|
|
7771
7777
|
}
|
|
7772
7778
|
}
|
|
@@ -7774,41 +7780,71 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7774
7780
|
}
|
|
7775
7781
|
|
|
7776
7782
|
// src/ThrottleBucket.ts
|
|
7777
|
-
class
|
|
7783
|
+
class ConcurrentThrottleBucket {
|
|
7778
7784
|
tokens;
|
|
7779
7785
|
maxTokens;
|
|
7780
|
-
refillRate;
|
|
7786
|
+
refillRate;
|
|
7781
7787
|
lastRefill;
|
|
7782
|
-
|
|
7788
|
+
semaphore;
|
|
7783
7789
|
constructor(rate, burst) {
|
|
7784
7790
|
this.tokens = burst;
|
|
7785
7791
|
this.maxTokens = burst;
|
|
7786
7792
|
this.refillRate = rate;
|
|
7787
7793
|
this.lastRefill = Date.now();
|
|
7794
|
+
this.semaphore = new Semaphore(burst);
|
|
7788
7795
|
}
|
|
7789
7796
|
refill() {
|
|
7790
7797
|
const now = Date.now();
|
|
7791
|
-
const elapsed = (now - this.lastRefill) / 1000;
|
|
7798
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
7792
7799
|
const newTokens = elapsed * this.refillRate;
|
|
7793
7800
|
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
7794
7801
|
this.lastRefill = now;
|
|
7795
7802
|
}
|
|
7796
7803
|
async acquire() {
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
this.pending = (async () => {
|
|
7800
|
-
await previous; // Wait for prior calls to complete
|
|
7804
|
+
await this.semaphore.acquire();
|
|
7805
|
+
try {
|
|
7801
7806
|
this.refill();
|
|
7802
|
-
|
|
7803
|
-
this.tokens
|
|
7804
|
-
|
|
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();
|
|
7805
7811
|
}
|
|
7806
|
-
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
|
|
7807
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
7808
|
-
this.refill();
|
|
7809
7812
|
this.tokens -= 1;
|
|
7810
|
-
}
|
|
7811
|
-
|
|
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;
|
|
7812
7848
|
}
|
|
7813
7849
|
}
|
|
7814
7850
|
|
|
@@ -7816,7 +7852,8 @@ class RateLimiter {
|
|
|
7816
7852
|
throttleBucket;
|
|
7817
7853
|
maxRetries;
|
|
7818
7854
|
retryDelay;
|
|
7819
|
-
constructor(throttleBucket,
|
|
7855
|
+
constructor(throttleBucket, // Updated type
|
|
7856
|
+
maxRetries = 3, retryDelay = 1000) {
|
|
7820
7857
|
this.throttleBucket = throttleBucket;
|
|
7821
7858
|
this.maxRetries = maxRetries;
|
|
7822
7859
|
this.retryDelay = retryDelay;
|
|
@@ -7826,8 +7863,12 @@ class RateLimiter {
|
|
|
7826
7863
|
await this.throttleBucket.acquire();
|
|
7827
7864
|
}
|
|
7828
7865
|
}
|
|
7866
|
+
release() {
|
|
7867
|
+
if (this.throttleBucket) {
|
|
7868
|
+
this.throttleBucket.release();
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7829
7871
|
async handle429(error, attempt) {
|
|
7830
|
-
// Remove the throw here; let invokeMethod handle exhaustion
|
|
7831
7872
|
const retryAfter = error.response.headers.get("Retry-After");
|
|
7832
7873
|
return retryAfter
|
|
7833
7874
|
? parseInt(retryAfter, 10) * 1000
|
|
@@ -7836,10 +7877,11 @@ class RateLimiter {
|
|
|
7836
7877
|
}
|
|
7837
7878
|
|
|
7838
7879
|
function quickbase(config) {
|
|
7839
|
-
const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate:
|
|
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;
|
|
7840
7882
|
const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
|
|
7841
7883
|
const throttleBucket = throttle
|
|
7842
|
-
? new
|
|
7884
|
+
? new ConcurrentThrottleBucket(throttle.rate, throttle.burst)
|
|
7843
7885
|
: null;
|
|
7844
7886
|
const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
|
|
7845
7887
|
const defaultFetch = typeof globalThis.window !== "undefined"
|