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.
Files changed (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +48 -18
  3. package/dist/esm/quickbase.d.ts +12 -3
  4. package/dist/esm/quickbase.js +416 -49
  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 +416 -49
  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/{ThrottleBucket.ts → FlowThrottleBucket.ts} +5 -35
  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 +94 -16
  20. package/src/pagination.ts +429 -0
  21. package/src/quickbaseClient.ts +56 -28
  22. package/src/rateLimiter.ts +3 -3
  23. package/tests/vitest/qb/quickbase-js/BurstAwareThrottleBucket.test.ts +125 -0
  24. package/tests/vitest/qb/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +1 -1
  25. package/tests/vitest/qb/quickbase-js/paginationSkip.test.ts +111 -0
  26. package/tests/vitest/qb/records/runQuery.test.ts +1 -2
  27. package/tests/vitest/unit/auth/retryUserToken401.test.ts +7 -10
  28. package/tests/vitest/unit/quickbase-js/BurstAwareThrottleBucket.test.ts +108 -0
  29. package/tests/vitest/unit/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +5 -5
  30. package/tests/vitest/unit/quickbase-js/pagination.test.ts +251 -0
  31. package/tests/vitest/unit/{quickbaseClientHttpMethods.test.ts → quickbase-js/quickbaseClientHttpMethods.test.ts} +13 -11
  32. package/tests/vitest/unit/records/runQuery.test.ts +3 -4
  33. package/tests/vitest/unit/umd.test.ts +0 -8
  34. /package/tests/vitest/unit/{rateLimit.test.ts → quickbase-js/rateLimit.test.ts} +0 -0
  35. /package/tests/vitest/unit/{tokenLifespan.test.ts → quickbase-js/tokenLifespan.test.ts} +0 -0
@@ -7478,6 +7478,8 @@ class TempTokenStrategy {
7478
7478
  const newToken = await this.getToken(dbid);
7479
7479
  if (newToken) {
7480
7480
  this.tokenCache.set(dbid, newToken);
7481
+ if (debug)
7482
+ console.log(`[${methodName || "method"}] Retrying with token: ${newToken.substring(0, 10)}...`);
7481
7483
  return newToken;
7482
7484
  }
7483
7485
  return null;
@@ -7555,6 +7557,8 @@ class SsoTokenStrategy {
7555
7557
  const newToken = await this.refreshSsoToken(debug || this.debug);
7556
7558
  if (newToken) {
7557
7559
  this.currentToken = newToken;
7560
+ if (debug || this.debug)
7561
+ console.log(`[${methodName || "method"}] Retrying with token: ${newToken.substring(0, 10)}...`);
7558
7562
  return newToken;
7559
7563
  }
7560
7564
  return null;
@@ -7640,20 +7644,318 @@ class RateLimitError extends Error {
7640
7644
  }
7641
7645
  }
7642
7646
 
7643
- // src/invokeMethod.ts
7644
- async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, attempt = 0, maxAttempts = rateLimiter.maxRetries + 1) {
7647
+ /**
7648
+ * Paginates records from a Quick Base API endpoint, handling both skip-based and token-based pagination.
7649
+ * @template K - The method name keyof QuickbaseClient.
7650
+ * @param methodName - The Quick Base API method to invoke (e.g., "runQuery", "getUsers").
7651
+ * @param params - Parameters for the API method, including optional skip and top values.
7652
+ * @param methodMap - Mapping of method names to their implementation details.
7653
+ * @param baseHeaders - Base HTTP headers for API requests.
7654
+ * @param authStrategy - Strategy for handling authentication tokens.
7655
+ * @param rateLimiter - Rate limiting mechanism to respect API limits.
7656
+ * @param transformDates - Function to transform date strings in the response.
7657
+ * @param debug - If true, logs detailed pagination information to the console.
7658
+ * @param convertDates - If true, converts date strings to Date objects in the response.
7659
+ * @param initialResponse - Optional initial response to start pagination from (used by invokeMethod).
7660
+ * @returns A promise resolving to the complete paginated response with all records.
7661
+ * @throws {Error} If no response is received during pagination.
7662
+ */
7663
+ async function paginateRecords(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, initialResponse) {
7664
+ if (debug)
7665
+ console.log("[paginateRecords] Starting with params:", params);
7666
+ /** Extracts the key containing the data array from the response (e.g., "data" or "users"). */
7667
+ const getDataKey = (response) => Object.keys(response).find((key) => Array.isArray(response[key])) || "data";
7668
+ const dataKey = initialResponse ? getDataKey(initialResponse) : "data";
7669
+ let allRecords = initialResponse ? initialResponse[dataKey] : [];
7670
+ let lastResponse = initialResponse;
7671
+ let requestCount = 0;
7672
+ /**
7673
+ * Determines the pagination strategy based on response metadata.
7674
+ * @param response - The API response to analyze.
7675
+ * @returns "skip" for skip-based, "token" for token-based, or "none" if no pagination is detected.
7676
+ */
7677
+ const determinePaginationType = (response) => {
7678
+ if (response.metadata.totalRecords !== undefined &&
7679
+ response.metadata.skip !== undefined) {
7680
+ if (debug)
7681
+ console.log("[paginateRecords] Detected skip-based pagination");
7682
+ return "skip";
7683
+ }
7684
+ if (response.metadata.nextPageToken !== undefined ||
7685
+ response.metadata.nextToken !== undefined) {
7686
+ if (debug)
7687
+ console.log("[paginateRecords] Detected token-based pagination");
7688
+ return "token";
7689
+ }
7690
+ if (debug)
7691
+ console.log("[paginateRecords] No clear pagination type detected, assuming single-page response");
7692
+ return "none"; // No pagination indicators
7693
+ };
7694
+ let paginationType = initialResponse
7695
+ ? determinePaginationType(initialResponse)
7696
+ : "none"; // Start with no pagination assumption
7697
+ let continuation = initialResponse
7698
+ ? paginationType === "skip"
7699
+ ? {
7700
+ type: "skip",
7701
+ value: (initialResponse.metadata.skip ?? 0) +
7702
+ (initialResponse.metadata.numRecords || allRecords.length),
7703
+ }
7704
+ : paginationType === "token"
7705
+ ? {
7706
+ type: "token",
7707
+ value: initialResponse.metadata.nextPageToken ||
7708
+ initialResponse.metadata.nextToken ||
7709
+ "",
7710
+ key: initialResponse.metadata.nextPageToken !== undefined
7711
+ ? "nextPageToken"
7712
+ : "nextToken",
7713
+ }
7714
+ : null
7715
+ : null; // No continuation until type is determined
7716
+ if (debug) {
7717
+ console.log("[paginateRecords] Initial state:", {
7718
+ dataKey,
7719
+ paginationType,
7720
+ continuation,
7721
+ hasInitialResponse: !!initialResponse,
7722
+ totalRecordsSoFar: allRecords.length,
7723
+ });
7724
+ if (initialResponse && initialResponse[dataKey].length > 0) {
7725
+ console.log("[paginateRecords] Initial response IDs: first:", initialResponse[dataKey][0]["3"]?.value, "last:", initialResponse[dataKey][initialResponse[dataKey].length - 1]["3"]
7726
+ ?.value);
7727
+ }
7728
+ }
7729
+ /** Fetches the next page of records using the provided parameters. */
7730
+ const fetchNextPage = async (paginatedParams) => {
7731
+ if (debug)
7732
+ console.log("[paginateRecords] Fetching with params:", paginatedParams);
7733
+ const response = await invokeMethod(methodName, paginatedParams, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, false, 0, rateLimiter.maxRetries + 1, true);
7734
+ requestCount++;
7735
+ return response;
7736
+ };
7737
+ /** Constructs the parameters for the next API request based on the pagination type. */
7738
+ const getPaginatedParams = () => {
7739
+ const hasBody = "body" in params && params.body !== undefined;
7740
+ if (paginationType === "skip" && continuation?.type === "skip") {
7741
+ const paginatedOptions = {
7742
+ ...(hasBody && "options" in params.body ? params.body.options : {}),
7743
+ skip: continuation.value,
7744
+ };
7745
+ return hasBody
7746
+ ? {
7747
+ ...params,
7748
+ body: {
7749
+ ...params.body,
7750
+ options: paginatedOptions,
7751
+ },
7752
+ }
7753
+ : { ...params, skip: continuation.value };
7754
+ }
7755
+ else if (paginationType === "token" && continuation?.type === "token") {
7756
+ if (continuation.value === "" && !lastResponse) {
7757
+ return { ...params }; // Initial call, no pagination fields
7758
+ }
7759
+ const tokenKey = continuation.key;
7760
+ return hasBody
7761
+ ? {
7762
+ ...params,
7763
+ body: {
7764
+ ...params.body,
7765
+ [tokenKey]: continuation.value,
7766
+ },
7767
+ }
7768
+ : { ...params, [tokenKey]: continuation.value };
7769
+ }
7770
+ return params; // No pagination or initial call
7771
+ };
7772
+ if (!initialResponse) {
7773
+ const firstParams = getPaginatedParams();
7774
+ const firstResponse = await fetchNextPage(firstParams);
7775
+ const actualDataKey = getDataKey(firstResponse);
7776
+ allRecords = firstResponse[actualDataKey];
7777
+ lastResponse = firstResponse;
7778
+ paginationType = determinePaginationType(firstResponse);
7779
+ continuation =
7780
+ paginationType === "skip"
7781
+ ? {
7782
+ type: "skip",
7783
+ value: (firstResponse.metadata.skip ?? 0) +
7784
+ (firstResponse.metadata.numRecords || allRecords.length),
7785
+ }
7786
+ : paginationType === "token"
7787
+ ? {
7788
+ type: "token",
7789
+ value: firstResponse.metadata.nextPageToken ??
7790
+ firstResponse.metadata.nextToken ??
7791
+ "",
7792
+ key: firstResponse.metadata.nextPageToken !== undefined
7793
+ ? "nextPageToken"
7794
+ : "nextToken",
7795
+ }
7796
+ : null;
7797
+ }
7798
+ while (true) {
7799
+ const hasMore = (paginationType === "skip" &&
7800
+ continuation?.type === "skip" &&
7801
+ lastResponse.metadata.totalRecords !== undefined &&
7802
+ continuation.value < lastResponse.metadata.totalRecords &&
7803
+ allRecords.length < lastResponse.metadata.totalRecords) ||
7804
+ (paginationType === "token" &&
7805
+ continuation?.type === "token" &&
7806
+ continuation.value !== "");
7807
+ if (!hasMore) {
7808
+ if (debug) {
7809
+ console.log("[paginateRecords] Stopping:", paginationType === "skip"
7810
+ ? "All skip-based records fetched or no more data"
7811
+ : paginationType === "token"
7812
+ ? "Token exhausted"
7813
+ : "No pagination required");
7814
+ }
7815
+ break;
7816
+ }
7817
+ const paginatedParams = getPaginatedParams();
7818
+ const response = await fetchNextPage(paginatedParams);
7819
+ const actualDataKey = getDataKey(response);
7820
+ allRecords = allRecords.concat(response[actualDataKey]);
7821
+ lastResponse = response;
7822
+ if (debug && response[actualDataKey].length > 0) {
7823
+ console.log("[paginateRecords] Fetched records this iteration:", response[actualDataKey].length, "IDs: first:", response[actualDataKey][0]["3"]?.value, "last:", response[actualDataKey][response[actualDataKey].length - 1]["3"]?.value, "Continuation after fetch:", continuation);
7824
+ }
7825
+ if (paginationType === "skip") {
7826
+ const newSkip = continuation?.type === "skip"
7827
+ ? continuation.value + response[actualDataKey].length
7828
+ : (response.metadata.skip ?? 0) + response[actualDataKey].length;
7829
+ continuation = { type: "skip", value: newSkip };
7830
+ if (response[actualDataKey].length === 0) {
7831
+ if (debug &&
7832
+ lastResponse.metadata.totalRecords &&
7833
+ newSkip < lastResponse.metadata.totalRecords) {
7834
+ console.warn("[paginateRecords] Warning: Empty response received but records remain", { skip: newSkip, totalRecords: lastResponse.metadata.totalRecords });
7835
+ }
7836
+ break;
7837
+ }
7838
+ }
7839
+ else if (paginationType === "token") {
7840
+ const newToken = response.metadata.nextPageToken ?? response.metadata.nextToken ?? "";
7841
+ continuation = {
7842
+ type: "token",
7843
+ value: newToken,
7844
+ key: response.metadata.nextPageToken !== undefined
7845
+ ? "nextPageToken"
7846
+ : "nextToken",
7847
+ };
7848
+ if (continuation.value === "") {
7849
+ break;
7850
+ }
7851
+ else if (response[actualDataKey].length === 0 && newToken !== "") {
7852
+ if (debug)
7853
+ console.warn("[paginateRecords] Warning: Empty response with non-empty token, possible API inconsistency", { token: newToken });
7854
+ }
7855
+ }
7856
+ }
7857
+ if (!lastResponse) {
7858
+ throw new Error("[paginateRecords] No response received during pagination");
7859
+ }
7860
+ const finalDataKey = getDataKey(lastResponse);
7861
+ const finalResponse = {
7862
+ [finalDataKey]: allRecords,
7863
+ fields: lastResponse.fields,
7864
+ metadata: {
7865
+ totalRecords: paginationType === "skip"
7866
+ ? lastResponse.metadata.totalRecords
7867
+ : undefined,
7868
+ numRecords: allRecords.length,
7869
+ numFields: lastResponse.metadata.numFields,
7870
+ skip: paginationType === "skip" ? 0 : undefined,
7871
+ top: paginationType === "skip" ? undefined : undefined,
7872
+ ...(paginationType === "token" &&
7873
+ continuation?.type === "token" &&
7874
+ continuation.key
7875
+ ? { [continuation.key]: "" }
7876
+ : {}),
7877
+ },
7878
+ };
7879
+ if (debug) {
7880
+ console.log("[paginateRecords] Final response summary:", {
7881
+ totalRecords: finalResponse.metadata.totalRecords,
7882
+ numRecords: finalResponse.metadata.numRecords,
7883
+ numFields: finalResponse.metadata.numFields,
7884
+ skip: finalResponse.metadata.skip,
7885
+ top: finalResponse.metadata.top,
7886
+ ...(paginationType === "token" &&
7887
+ continuation?.type === "token" &&
7888
+ continuation.key
7889
+ ? { [continuation.key]: finalResponse.metadata[continuation.key] }
7890
+ : {}),
7891
+ });
7892
+ console.log("[paginateRecords] Pagination stats:", {
7893
+ totalRequests: requestCount,
7894
+ });
7895
+ }
7896
+ return finalResponse;
7897
+ }
7898
+ /**
7899
+ * Checks if a response is paginatable based on its structure.
7900
+ * @param response - The response to check.
7901
+ * @returns True if the response has a data array and pagination metadata, false otherwise.
7902
+ */
7903
+ function isPaginatable(response) {
7904
+ return (response &&
7905
+ typeof response === "object" &&
7906
+ Object.keys(response).some((key) => Array.isArray(response[key])) &&
7907
+ "metadata" in response &&
7908
+ typeof response.metadata === "object" &&
7909
+ (("totalRecords" in response.metadata &&
7910
+ "numRecords" in response.metadata &&
7911
+ "skip" in response.metadata) ||
7912
+ "nextPageToken" in response.metadata ||
7913
+ "nextToken" in response.metadata));
7914
+ }
7915
+
7916
+ /**
7917
+ * Invokes a Quick Base API method with the specified parameters, handling authentication, rate limiting, and pagination.
7918
+ * @template K - The method name keyof QuickbaseClient.
7919
+ * @param methodName - The API method to invoke (e.g., "runQuery", "changesetSolution").
7920
+ * @param params - Parameters for the API method, potentially including skip, top, or a body.
7921
+ * @param methodMap - Mapping of method names to their implementation details.
7922
+ * @param baseHeaders - Base HTTP headers for API requests.
7923
+ * @param authStrategy - Strategy for handling authentication tokens.
7924
+ * @param rateLimiter - Rate limiting mechanism to respect API limits.
7925
+ * @param transformDates - Function to transform date strings in the response.
7926
+ * @param debug - If true, logs detailed invocation information.
7927
+ * @param convertDates - If true, converts date strings to Date objects in the response.
7928
+ * @param autoPaginate - If true, automatically paginates multi-page responses (default: true).
7929
+ * @param attempt - Current retry attempt (default: 0).
7930
+ * @param maxAttempts - Maximum number of retry attempts (default: rateLimiter.maxRetries + 1).
7931
+ * @param isPaginating - Indicates if this call is part of an ongoing pagination (default: false).
7932
+ * @returns A promise resolving to the API method's response.
7933
+ * @throws {Error} If the method is not found or retries are exhausted.
7934
+ */
7935
+ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, autoPaginate = true, attempt = 0, maxAttempts = rateLimiter.maxRetries + 1, isPaginating = false) {
7645
7936
  const methodInfo = methodMap[methodName];
7646
7937
  if (!methodInfo)
7647
7938
  throw new Error(`Method ${methodName} not found`);
7648
7939
  const hasBody = "body" in params && params.body !== undefined;
7649
7940
  const body = hasBody ? params.body : undefined;
7941
+ // Safely check for options only if body is an object
7942
+ const options = hasBody && body && typeof body === "object" && "options" in body
7943
+ ? body.options
7944
+ : undefined;
7945
+ const adjustedParams = {
7946
+ ...params,
7947
+ skip: params.skip ?? (options && "skip" in options ? options.skip : undefined),
7948
+ top: params.top ?? (options && "top" in options ? options.top : undefined),
7949
+ };
7650
7950
  const restParams = hasBody
7651
- ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body" && key !== "startTime"))
7652
- : { ...params, startTime: undefined };
7951
+ ? Object.fromEntries(Object.entries(adjustedParams).filter(([key]) => key !== "body" && key !== "startTime"))
7952
+ : { ...adjustedParams, startTime: undefined };
7653
7953
  const requestParameters = {
7654
7954
  ...restParams,
7655
7955
  ...(hasBody ? { generated: body } : {}),
7656
7956
  };
7957
+ if (debug)
7958
+ console.log("[invokeMethod] Adjusted params for pagination:", adjustedParams);
7657
7959
  const requestOptions = {
7658
7960
  credentials: methodName === "getTempTokenDBID" ? "include" : "omit",
7659
7961
  method: methodInfo.httpMethod,
@@ -7676,12 +7978,22 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7676
7978
  ?.toLowerCase();
7677
7979
  if (contentType?.includes("application/json")) {
7678
7980
  const jsonResponse = await rawResponse.json();
7981
+ if (autoPaginate && !isPaginating && isPaginatable(jsonResponse)) {
7982
+ if (debug)
7983
+ console.log("[invokeMethod] Entering pagination with params:", adjustedParams);
7984
+ return paginateRecords(methodName, adjustedParams, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, jsonResponse);
7985
+ }
7679
7986
  return transformDates(jsonResponse, convertDates);
7680
7987
  }
7681
7988
  return rawResponse;
7682
7989
  }
7683
7990
  if (rawResponse && typeof rawResponse.value === "function") {
7684
7991
  const response = await rawResponse.value();
7992
+ if (autoPaginate && !isPaginating && isPaginatable(response)) {
7993
+ if (debug)
7994
+ console.log("[invokeMethod] Entering pagination with params:", adjustedParams);
7995
+ return paginateRecords(methodName, adjustedParams, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, response);
7996
+ }
7685
7997
  return transformDates(response, convertDates);
7686
7998
  }
7687
7999
  return transformDates(rawResponse, convertDates);
@@ -7718,11 +8030,18 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7718
8030
  try {
7719
8031
  await rateLimiter.throttle();
7720
8032
  acquired = true;
7721
- const postThrottleTime = Date.now(); // Capture post-throttle time
8033
+ const postThrottleTime = Date.now();
7722
8034
  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
8035
+ params.startTime = postThrottleTime;
8036
+ const headers = { ...baseHeaders };
8037
+ const finalRequest = { ...requestParameters, headers };
8038
+ if (debug)
8039
+ console.log("[invokeMethod] Sending request:", finalRequest);
8040
+ const responsePromise = methodInfo.method(finalRequest, {
8041
+ ...requestOptions,
8042
+ headers,
8043
+ });
8044
+ rateLimiter.release();
7726
8045
  const response = await responsePromise;
7727
8046
  return await processResponse(response);
7728
8047
  }
@@ -7768,8 +8087,6 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7768
8087
  token = newToken;
7769
8088
  authStrategy.applyHeaders(baseHeaders, token);
7770
8089
  requestOptions.headers = { ...baseHeaders };
7771
- if (debug)
7772
- console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
7773
8090
  attempt++;
7774
8091
  continue;
7775
8092
  }
@@ -7779,8 +8096,37 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7779
8096
  throw new Error(`API Error: Exhausted retries after ${maxAttempts} attempts`);
7780
8097
  }
7781
8098
 
7782
- // src/ThrottleBucket.ts
7783
- class ConcurrentThrottleBucket {
8099
+ // src/Semaphore.ts
8100
+ class Semaphore {
8101
+ permits;
8102
+ waiting = [];
8103
+ constructor(maxPermits) {
8104
+ this.permits = maxPermits;
8105
+ }
8106
+ async acquire() {
8107
+ if (this.permits > 0) {
8108
+ this.permits -= 1;
8109
+ return;
8110
+ }
8111
+ return new Promise((resolve, reject) => {
8112
+ this.waiting.push({ resolve, reject });
8113
+ });
8114
+ }
8115
+ release() {
8116
+ this.permits += 1;
8117
+ if (this.waiting.length > 0 && this.permits > 0) {
8118
+ const { resolve } = this.waiting.shift();
8119
+ this.permits -= 1;
8120
+ resolve();
8121
+ }
8122
+ }
8123
+ available() {
8124
+ return this.permits;
8125
+ }
8126
+ }
8127
+
8128
+ // src/FlowThrottleBucket.ts
8129
+ class FlowThrottleBucket {
7784
8130
  tokens;
7785
8131
  maxTokens;
7786
8132
  refillRate;
@@ -7820,31 +8166,46 @@ class ConcurrentThrottleBucket {
7820
8166
  this.semaphore.release();
7821
8167
  }
7822
8168
  }
7823
- class Semaphore {
7824
- permits;
7825
- waiting = [];
7826
- constructor(maxPermits) {
7827
- this.permits = maxPermits;
8169
+
8170
+ // src/BurstAwareThrottleBucket.ts
8171
+ class BurstAwareThrottleBucket {
8172
+ maxTokens;
8173
+ windowSeconds;
8174
+ requestTimestamps = [];
8175
+ semaphore;
8176
+ constructor(options) {
8177
+ this.maxTokens = options.maxTokens;
8178
+ this.windowSeconds = options.windowSeconds;
8179
+ this.semaphore = new Semaphore(this.maxTokens);
8180
+ }
8181
+ countRequestsInWindow() {
8182
+ const now = Date.now();
8183
+ this.requestTimestamps = this.requestTimestamps.filter((ts) => now - ts < this.windowSeconds * 1000);
8184
+ return this.requestTimestamps.length;
8185
+ }
8186
+ availableTokens() {
8187
+ const windowCount = this.countRequestsInWindow();
8188
+ return Math.max(0, Math.min(100 - windowCount, this.maxTokens - windowCount));
7828
8189
  }
7829
8190
  async acquire() {
7830
- if (this.permits > 0) {
7831
- this.permits -= 1;
7832
- return;
8191
+ while (true) {
8192
+ const available = this.availableTokens();
8193
+ if (available > 0 && this.semaphore.available() > 0) {
8194
+ await this.semaphore.acquire();
8195
+ this.requestTimestamps.push(Date.now());
8196
+ return;
8197
+ }
8198
+ const now = Date.now();
8199
+ const oldestTimestamp = this.requestTimestamps[0];
8200
+ let waitTime = 100;
8201
+ if (oldestTimestamp) {
8202
+ waitTime = Math.max(0, this.windowSeconds * 1000 - (now - oldestTimestamp));
8203
+ }
8204
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
7833
8205
  }
7834
- return new Promise((resolve, reject) => {
7835
- this.waiting.push({ resolve, reject });
7836
- });
7837
8206
  }
7838
8207
  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;
8208
+ this.semaphore.release();
7848
8209
  }
7849
8210
  }
7850
8211
 
@@ -7852,8 +8213,7 @@ class RateLimiter {
7852
8213
  throttleBucket;
7853
8214
  maxRetries;
7854
8215
  retryDelay;
7855
- constructor(throttleBucket, // Updated type
7856
- maxRetries = 3, retryDelay = 1000) {
8216
+ constructor(throttleBucket, maxRetries = 3, retryDelay = 1000) {
7857
8217
  this.throttleBucket = throttleBucket;
7858
8218
  this.maxRetries = maxRetries;
7859
8219
  this.retryDelay = retryDelay;
@@ -7877,20 +8237,23 @@ class RateLimiter {
7877
8237
  }
7878
8238
 
7879
8239
  function quickbase(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;
8240
+ const { realm, userToken, tempToken, useTempTokens = false, useSso = false, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { type: "flow", rate: 5, burst: 50 }, // Changed rate from 6 to 5
8241
+ maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", autoPaginate = true, } = config;
7882
8242
  const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
7883
- const throttleBucket = throttle
7884
- ? new ConcurrentThrottleBucket(throttle.rate, throttle.burst)
7885
- : null;
8243
+ const throttleOptions = throttle;
8244
+ const throttleBucket = throttleOptions.type === "burst-aware"
8245
+ ? new BurstAwareThrottleBucket({
8246
+ maxTokens: throttleOptions.burst || 100,
8247
+ windowSeconds: throttleOptions.windowSeconds || 10,
8248
+ })
8249
+ : new FlowThrottleBucket(throttleOptions.rate || 5, // Changed rate from 6 to 5
8250
+ throttleOptions.burst || 50);
7886
8251
  const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
7887
- const defaultFetch = typeof globalThis.window !== "undefined"
7888
- ? globalThis.window.fetch.bind(globalThis.window)
7889
- : undefined;
7890
- const effectiveFetch = fetchApi || defaultFetch;
7891
- if (!effectiveFetch) {
7892
- throw new Error("No fetch implementation available");
8252
+ const defaultFetch = globalThis.fetch || globalThis.window?.fetch?.bind(globalThis.window);
8253
+ if (!defaultFetch && !fetchApi) {
8254
+ throw new Error("No fetch implementation available. Please provide fetchApi in a Node.js environment without native fetch.");
7893
8255
  }
8256
+ const effectiveFetch = fetchApi || defaultFetch;
7894
8257
  const authStrategy = useSso
7895
8258
  ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
7896
8259
  : useTempTokens
@@ -7945,19 +8308,23 @@ function quickbase(config) {
7945
8308
  const methodMap = buildMethodMap();
7946
8309
  const proxyHandler = {
7947
8310
  get: (_, prop) => {
7948
- console.log("[proxy] Accessing:", prop, "in methodMap:", prop in methodMap);
8311
+ if (debug) {
8312
+ console.log("[proxy] Accessing:", prop, "in methodMap:", prop in methodMap);
8313
+ }
7949
8314
  if (prop in methodMap) {
7950
8315
  const methodName = prop;
7951
- return (params) => invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates);
8316
+ return (params) => invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, autoPaginate);
8317
+ }
8318
+ if (debug) {
8319
+ console.log("[proxy] Method not found:", prop);
7952
8320
  }
7953
- console.log("[proxy] Method not found:", prop);
7954
8321
  return undefined;
7955
8322
  },
7956
8323
  };
7957
8324
  const proxy = new Proxy({}, proxyHandler);
7958
8325
  if (debug) {
7959
8326
  console.log("[createClient] Config:", config);
7960
- console.log("[createClient] Proxy created:", proxy);
8327
+ console.log("[createClient] Returning proxy");
7961
8328
  }
7962
8329
  return proxy;
7963
8330
  }