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
@@ -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"))
7652
- : { ...params };
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);
@@ -7714,12 +8026,28 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7714
8026
  return { message, status };
7715
8027
  }
7716
8028
  while (attempt < maxAttempts) {
8029
+ let acquired = false;
7717
8030
  try {
7718
8031
  await rateLimiter.throttle();
7719
- const response = await methodInfo.method(requestParameters, requestOptions);
8032
+ acquired = true;
8033
+ const postThrottleTime = Date.now();
8034
+ if (params.startTime !== undefined)
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();
8045
+ const response = await responsePromise;
7720
8046
  return await processResponse(response);
7721
8047
  }
7722
8048
  catch (error) {
8049
+ if (acquired)
8050
+ rateLimiter.release();
7723
8051
  let status;
7724
8052
  let message;
7725
8053
  let response;
@@ -7732,7 +8060,7 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7732
8060
  ({ message, status } = await parseErrorResponse(response));
7733
8061
  }
7734
8062
  else {
7735
- throw error; // Rethrow truly unexpected errors
8063
+ throw error;
7736
8064
  }
7737
8065
  if (status === 429) {
7738
8066
  if (!(error instanceof ResponseError)) {
@@ -7748,67 +8076,136 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7748
8076
  attempt++;
7749
8077
  continue;
7750
8078
  }
7751
- // Handle authentication errors
7752
8079
  let newToken;
7753
8080
  try {
7754
8081
  newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
7755
8082
  }
7756
8083
  catch (authError) {
7757
- throw authError; // Propagate the fetchTempToken error and exit immediately
8084
+ throw authError;
7758
8085
  }
7759
- // Only proceed with retry if we have a valid new token
7760
8086
  if (newToken !== null) {
7761
8087
  token = newToken;
7762
8088
  authStrategy.applyHeaders(baseHeaders, token);
7763
8089
  requestOptions.headers = { ...baseHeaders };
7764
- if (debug)
7765
- console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
7766
8090
  attempt++;
7767
8091
  continue;
7768
8092
  }
7769
- // If no new token was provided, throw the original error
7770
8093
  throw new Error(`API Error: ${message} (Status: ${status})`);
7771
8094
  }
7772
8095
  }
7773
8096
  throw new Error(`API Error: Exhausted retries after ${maxAttempts} attempts`);
7774
8097
  }
7775
8098
 
7776
- // src/ThrottleBucket.ts
7777
- class ThrottleBucket {
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 {
7778
8130
  tokens;
7779
8131
  maxTokens;
7780
- refillRate; // Tokens per second
8132
+ refillRate;
7781
8133
  lastRefill;
7782
- pending = Promise.resolve(); // Queue for sequential execution
8134
+ semaphore;
7783
8135
  constructor(rate, burst) {
7784
8136
  this.tokens = burst;
7785
8137
  this.maxTokens = burst;
7786
8138
  this.refillRate = rate;
7787
8139
  this.lastRefill = Date.now();
8140
+ this.semaphore = new Semaphore(burst);
7788
8141
  }
7789
8142
  refill() {
7790
8143
  const now = Date.now();
7791
- const elapsed = (now - this.lastRefill) / 1000; // Seconds elapsed
8144
+ const elapsed = (now - this.lastRefill) / 1000;
7792
8145
  const newTokens = elapsed * this.refillRate;
7793
8146
  this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
7794
8147
  this.lastRefill = now;
7795
8148
  }
7796
8149
  async acquire() {
7797
- // Chain the new acquisition onto the pending queue
7798
- const previous = this.pending;
7799
- this.pending = (async () => {
7800
- await previous; // Wait for prior calls to complete
8150
+ await this.semaphore.acquire();
8151
+ try {
7801
8152
  this.refill();
7802
- if (this.tokens >= 1) {
7803
- this.tokens -= 1;
8153
+ while (this.tokens < 1) {
8154
+ const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
8155
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
8156
+ this.refill();
8157
+ }
8158
+ this.tokens -= 1;
8159
+ }
8160
+ catch (error) {
8161
+ this.semaphore.release();
8162
+ throw error;
8163
+ }
8164
+ }
8165
+ release() {
8166
+ this.semaphore.release();
8167
+ }
8168
+ }
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));
8189
+ }
8190
+ async acquire() {
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());
7804
8196
  return;
7805
8197
  }
7806
- const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
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
+ }
7807
8204
  await new Promise((resolve) => setTimeout(resolve, waitTime));
7808
- this.refill();
7809
- this.tokens -= 1;
7810
- })();
7811
- await this.pending;
8205
+ }
8206
+ }
8207
+ release() {
8208
+ this.semaphore.release();
7812
8209
  }
7813
8210
  }
7814
8211
 
@@ -7826,8 +8223,12 @@ class RateLimiter {
7826
8223
  await this.throttleBucket.acquire();
7827
8224
  }
7828
8225
  }
8226
+ release() {
8227
+ if (this.throttleBucket) {
8228
+ this.throttleBucket.release();
8229
+ }
8230
+ }
7829
8231
  async handle429(error, attempt) {
7830
- // Remove the throw here; let invokeMethod handle exhaustion
7831
8232
  const retryAfter = error.response.headers.get("Retry-After");
7832
8233
  return retryAfter
7833
8234
  ? parseInt(retryAfter, 10) * 1000
@@ -7836,19 +8237,23 @@ class RateLimiter {
7836
8237
  }
7837
8238
 
7838
8239
  function quickbase(config) {
7839
- 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;
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;
7840
8242
  const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
7841
- const throttleBucket = throttle
7842
- ? new ThrottleBucket(throttle.rate, throttle.burst)
7843
- : 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);
7844
8251
  const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
7845
- const defaultFetch = typeof globalThis.window !== "undefined"
7846
- ? globalThis.window.fetch.bind(globalThis.window)
7847
- : undefined;
7848
- const effectiveFetch = fetchApi || defaultFetch;
7849
- if (!effectiveFetch) {
7850
- 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.");
7851
8255
  }
8256
+ const effectiveFetch = fetchApi || defaultFetch;
7852
8257
  const authStrategy = useSso
7853
8258
  ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
7854
8259
  : useTempTokens
@@ -7903,19 +8308,23 @@ function quickbase(config) {
7903
8308
  const methodMap = buildMethodMap();
7904
8309
  const proxyHandler = {
7905
8310
  get: (_, prop) => {
7906
- 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
+ }
7907
8314
  if (prop in methodMap) {
7908
8315
  const methodName = prop;
7909
- 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);
7910
8320
  }
7911
- console.log("[proxy] Method not found:", prop);
7912
8321
  return undefined;
7913
8322
  },
7914
8323
  };
7915
8324
  const proxy = new Proxy({}, proxyHandler);
7916
8325
  if (debug) {
7917
8326
  console.log("[createClient] Config:", config);
7918
- console.log("[createClient] Proxy created:", proxy);
8327
+ console.log("[createClient] Returning proxy");
7919
8328
  }
7920
8329
  return proxy;
7921
8330
  }