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.
- package/CHANGELOG.md +16 -0
- package/README.md +56 -24
- package/dist/esm/quickbase.d.ts +1 -0
- package/dist/esm/quickbase.js +125 -87
- 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 +125 -87
- 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/examples/quicbase-jsCDN.html +2 -2
- package/package.json +1 -1
- package/src/ThrottleBucket.ts +52 -16
- package/src/authorizationStrategy.ts +62 -35
- package/src/generated-unified/QuickbaseClient.ts +1 -1
- package/src/invokeMethod.ts +33 -66
- package/src/quickbaseClient.ts +12 -7
- package/src/rateLimiter.ts +8 -3
- package/src/tokenCache.ts +4 -0
- package/tests/playwright/qb/auth/getAppWithTempTokenConcurrency.test.ts +208 -0
- package/tests/vitest/qb/ThrottleBucket.test.ts +77 -0
- package/tests/vitest/qb/auth/ssoTokenConcurrency.test.ts +68 -0
- package/tests/vitest/qb/auth/userTokenConcurrency.test.ts +63 -0
- package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
- package/tests/vitest/unit/ThrottleBucket.test.ts +16 -10
- package/tests/vitest/unit/apps/getAppConcurrency.test.ts +151 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +20 -2
- package/tests/vitest/unit/auth/userTokenConcurrency.test.ts +62 -0
- package/tests/vitest/unit/index.ts +1 -1
- package/tests/vitest/unit/quickbaseClientHttpMethods.test.ts +3 -3
- package/tools/log-gen.ts +1 -1
- package/tools/log-gen.yaml +130 -78
- package/api-examples.md +0 -49
- package/markdown-examples.md +0 -85
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
if (typeof QuickbaseJS === "undefined") {
|
|
18
18
|
document.getElementById("result").textContent = "Error: Failed to load QuickbaseJS library.";
|
|
19
19
|
} else {
|
|
20
|
-
var
|
|
20
|
+
var qb = QuickbaseJS.quickbase({
|
|
21
21
|
realm,
|
|
22
22
|
useTempTokens: true
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
qb.getApp({ appId })
|
|
26
26
|
.then(function (app) {
|
|
27
27
|
document.getElementById("result").textContent = "App Name: " + app.name;
|
|
28
28
|
})
|
package/package.json
CHANGED
package/src/ThrottleBucket.ts
CHANGED
|
@@ -1,41 +1,77 @@
|
|
|
1
1
|
// src/ThrottleBucket.ts
|
|
2
|
-
export class
|
|
2
|
+
export class ConcurrentThrottleBucket {
|
|
3
3
|
private tokens: number;
|
|
4
4
|
private maxTokens: number;
|
|
5
|
-
private refillRate: number;
|
|
5
|
+
private refillRate: number;
|
|
6
6
|
private lastRefill: number;
|
|
7
|
-
private
|
|
7
|
+
private semaphore: Semaphore;
|
|
8
8
|
|
|
9
9
|
constructor(rate: number, burst: number) {
|
|
10
10
|
this.tokens = burst;
|
|
11
11
|
this.maxTokens = burst;
|
|
12
12
|
this.refillRate = rate;
|
|
13
13
|
this.lastRefill = Date.now();
|
|
14
|
+
this.semaphore = new Semaphore(burst);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
private refill(): void {
|
|
17
18
|
const now = Date.now();
|
|
18
|
-
const elapsed = (now - this.lastRefill) / 1000;
|
|
19
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
19
20
|
const newTokens = elapsed * this.refillRate;
|
|
20
21
|
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
21
22
|
this.lastRefill = now;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async acquire(): Promise<void> {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.pending = (async () => {
|
|
28
|
-
await previous; // Wait for prior calls to complete
|
|
26
|
+
await this.semaphore.acquire();
|
|
27
|
+
try {
|
|
29
28
|
this.refill();
|
|
30
|
-
|
|
31
|
-
this.tokens
|
|
32
|
-
|
|
29
|
+
while (this.tokens < 1) {
|
|
30
|
+
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
32
|
+
this.refill();
|
|
33
33
|
}
|
|
34
|
-
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
|
|
35
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
36
|
-
this.refill();
|
|
37
34
|
this.tokens -= 1;
|
|
38
|
-
}
|
|
39
|
-
|
|
35
|
+
} catch (error) {
|
|
36
|
+
this.semaphore.release();
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
release(): void {
|
|
42
|
+
this.semaphore.release();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class Semaphore {
|
|
47
|
+
private permits: number;
|
|
48
|
+
private waiting: Array<{ resolve: () => void; reject: (err: any) => void }> =
|
|
49
|
+
[];
|
|
50
|
+
|
|
51
|
+
constructor(maxPermits: number) {
|
|
52
|
+
this.permits = maxPermits;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async acquire(): Promise<void> {
|
|
56
|
+
if (this.permits > 0) {
|
|
57
|
+
this.permits -= 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
this.waiting.push({ resolve, reject });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
release(): void {
|
|
66
|
+
this.permits += 1;
|
|
67
|
+
if (this.waiting.length > 0 && this.permits > 0) {
|
|
68
|
+
const { resolve } = this.waiting.shift()!;
|
|
69
|
+
this.permits -= 1;
|
|
70
|
+
resolve();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
available(): number {
|
|
75
|
+
return this.permits;
|
|
40
76
|
}
|
|
41
77
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/authorizationStrategy.ts
|
|
2
|
-
|
|
3
2
|
import { TokenCache } from "./tokenCache";
|
|
4
3
|
|
|
5
4
|
export interface AuthorizationStrategy {
|
|
@@ -15,9 +14,9 @@ export interface AuthorizationStrategy {
|
|
|
15
14
|
): Promise<string | null>;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
// src/authorizationStrategy.ts (partial update)
|
|
19
|
-
|
|
20
17
|
export class TempTokenStrategy implements AuthorizationStrategy {
|
|
18
|
+
private pendingFetches: Map<string, Promise<string>> = new Map();
|
|
19
|
+
|
|
21
20
|
constructor(
|
|
22
21
|
private tokenCache: TokenCache,
|
|
23
22
|
private initialTempToken: string | undefined,
|
|
@@ -61,7 +60,15 @@ export class TempTokenStrategy implements AuthorizationStrategy {
|
|
|
61
60
|
async getToken(dbid: string): Promise<string | undefined> {
|
|
62
61
|
let token = this.tokenCache.get(dbid) || this.initialTempToken;
|
|
63
62
|
if (!token && dbid) {
|
|
64
|
-
|
|
63
|
+
if (this.pendingFetches.has(dbid)) {
|
|
64
|
+
console.log(`[getToken] Waiting for existing fetch for dbid: ${dbid}`);
|
|
65
|
+
return this.pendingFetches.get(dbid);
|
|
66
|
+
}
|
|
67
|
+
const fetchPromise = this.fetchTempToken(dbid).finally(() =>
|
|
68
|
+
this.pendingFetches.delete(dbid)
|
|
69
|
+
);
|
|
70
|
+
this.pendingFetches.set(dbid, fetchPromise);
|
|
71
|
+
token = await fetchPromise;
|
|
65
72
|
}
|
|
66
73
|
return token;
|
|
67
74
|
}
|
|
@@ -96,14 +103,31 @@ export class TempTokenStrategy implements AuthorizationStrategy {
|
|
|
96
103
|
return null;
|
|
97
104
|
}
|
|
98
105
|
if (debug) console.log(`Refreshing temp token for dbid: ${dbid}`);
|
|
99
|
-
|
|
100
|
-
this.tokenCache.
|
|
101
|
-
|
|
106
|
+
// Invalidate the cache for this dbid to force a fresh fetch
|
|
107
|
+
this.tokenCache.delete(dbid);
|
|
108
|
+
try {
|
|
109
|
+
const newToken = await this.getToken(dbid);
|
|
110
|
+
if (newToken) {
|
|
111
|
+
this.tokenCache.set(dbid, newToken);
|
|
112
|
+
return newToken;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (debug)
|
|
117
|
+
console.log(
|
|
118
|
+
`[${methodName || "method"}] Failed to refresh token:`,
|
|
119
|
+
error
|
|
120
|
+
);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
102
123
|
}
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
export class UserTokenStrategy implements AuthorizationStrategy {
|
|
106
|
-
constructor(
|
|
127
|
+
constructor(
|
|
128
|
+
private userToken: string,
|
|
129
|
+
private baseUrl: string = "https://api.quickbase.com/v1"
|
|
130
|
+
) {}
|
|
107
131
|
|
|
108
132
|
async getToken(_dbid: string): Promise<string> {
|
|
109
133
|
return this.userToken;
|
|
@@ -134,23 +158,32 @@ export class UserTokenStrategy implements AuthorizationStrategy {
|
|
|
134
158
|
|
|
135
159
|
export class SsoTokenStrategy implements AuthorizationStrategy {
|
|
136
160
|
private currentToken: string | undefined;
|
|
161
|
+
private pendingFetches: Map<string, Promise<string>> = new Map();
|
|
137
162
|
|
|
138
163
|
constructor(
|
|
139
164
|
private samlToken: string,
|
|
140
165
|
private realm: string,
|
|
141
166
|
private fetchApi: typeof fetch,
|
|
142
|
-
private debug: boolean = false
|
|
167
|
+
private debug: boolean = false,
|
|
168
|
+
private baseUrl: string = "https://api.quickbase.com/v1"
|
|
143
169
|
) {}
|
|
144
170
|
|
|
145
171
|
async getToken(_dbid: string): Promise<string | undefined> {
|
|
146
172
|
if (!this.currentToken) {
|
|
147
|
-
|
|
173
|
+
if (this.pendingFetches.has("sso")) {
|
|
174
|
+
if (this.debug)
|
|
175
|
+
console.log("[getToken] Waiting for existing SSO fetch");
|
|
176
|
+
return this.pendingFetches.get("sso");
|
|
177
|
+
}
|
|
178
|
+
const fetchPromise = this.fetchSsoToken({
|
|
148
179
|
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
149
180
|
requested_token_type:
|
|
150
181
|
"urn:quickbase:params:oauth:token-type:temp_token",
|
|
151
182
|
subject_token: this.samlToken,
|
|
152
183
|
subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
|
|
153
|
-
});
|
|
184
|
+
}).finally(() => this.pendingFetches.delete("sso"));
|
|
185
|
+
this.pendingFetches.set("sso", fetchPromise);
|
|
186
|
+
this.currentToken = await fetchPromise;
|
|
154
187
|
}
|
|
155
188
|
return this.currentToken;
|
|
156
189
|
}
|
|
@@ -193,18 +226,15 @@ export class SsoTokenStrategy implements AuthorizationStrategy {
|
|
|
193
226
|
subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
|
|
194
227
|
};
|
|
195
228
|
|
|
196
|
-
const response = await this.fetchApi(
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
credentials: "omit",
|
|
206
|
-
}
|
|
207
|
-
);
|
|
229
|
+
const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: {
|
|
232
|
+
"QB-Realm-Hostname": `${this.realm}.quickbase.com`,
|
|
233
|
+
"Content-Type": "application/json",
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify(payload),
|
|
236
|
+
credentials: "omit",
|
|
237
|
+
});
|
|
208
238
|
|
|
209
239
|
if (!response.ok) {
|
|
210
240
|
const errorBody = await response.json().catch(() => ({}));
|
|
@@ -234,18 +264,15 @@ export class SsoTokenStrategy implements AuthorizationStrategy {
|
|
|
234
264
|
subject_token: string;
|
|
235
265
|
subject_token_type: string;
|
|
236
266
|
}): Promise<string> {
|
|
237
|
-
const response = await this.fetchApi(
|
|
238
|
-
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
credentials: "omit",
|
|
247
|
-
}
|
|
248
|
-
);
|
|
267
|
+
const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: {
|
|
270
|
+
"QB-Realm-Hostname": `${this.realm}.quickbase.com`,
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify(params),
|
|
274
|
+
credentials: "omit",
|
|
275
|
+
});
|
|
249
276
|
|
|
250
277
|
if (!response.ok) {
|
|
251
278
|
const errorBody = await response.json().catch(() => ({}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated on 2025-03-
|
|
1
|
+
// Generated on 2025-03-23T22:51:37.128Z
|
|
2
2
|
import { AddManagersToGroup200Response, AddManagersToGroupRequest, AddMembersToGroup200Response, AddMembersToGroupRequest, AddSubgroupsToGroup200Response, AddSubgroupsToGroupRequest, Audit200Response, ChangesetSolution200Response, ChangesetSolutionFromRecord200Response, CloneUserToken200Response, CopyApp200Response, CopyAppRequest, CreateApp200Response, CreateAppRequest, CreateField200Response, CreateFieldRequest, CreateRelationship200Response, CreateRelationshipRequest, CreateSolution200Response, CreateSolutionFromRecord200Response, CreateTable200Response, CreateTableRequest, DeactivateUserToken200Response, DeleteApp200Response, DeleteAppRequest, DeleteFields200Response, DeleteFieldsRequest, DeleteFile200Response, DeleteRecords200Response, DeleteRecordsRequest, DeleteRelationship200Response, DeleteTable200Response, DeleteUserToken200Response, DenyUsers200Response, DenyUsersAndGroups200Response, DenyUsersAndGroupsRequest, DenyUsersRequest, DownloadFile200Response, ExchangeSsoToken200Response, ExportSolution200Response, ExportSolutionToRecord200Response, GenerateDocument200Response, GetApp200Response, GetAppEvents200Response, GetAppTables200Response, GetField200Response, GetFieldUsage200Response, GetFields200Response, GetFieldsUsage200Response, GetRelationships200Response, GetReport200Response, GetTable200Response, GetTableReports200Response, GetTempTokenDBID200Response, GetUsers200Response, GetUsersRequest, PlatformAnalyticEventSummaries200Response, PlatformAnalyticReads200Response, RemoveManagersFromGroup200Response, RemoveManagersFromGroupRequest, RemoveMembersFromGroup200Response, RemoveMembersFromGroupRequest, RemoveSubgroupsFromGroup200Response, RemoveSubgroupsFromGroupRequest, RunFormula200Response, RunQuery200Response, RunQueryRequest, RunReport200Response, TransferUserToken200Response, UndenyUsers200Response, UndenyUsersRequest, UpdateApp200Response, UpdateAppRequest, UpdateField200Response, UpdateFieldRequest, UpdateRelationship200Response, UpdateRelationshipRequest, UpdateSolution200Response, UpdateSolutionToRecord200Response, UpdateTable200Response, UpdateTableRequest, Upsert200Response, Upsert207Response, UpsertRequest } from "../generated/models";
|
|
3
3
|
|
|
4
4
|
export interface QuickbaseClient {
|
package/src/invokeMethod.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/invokeMethod.ts
|
|
2
|
-
|
|
3
2
|
import { AuthorizationStrategy, extractDbid } from "./authorizationStrategy";
|
|
4
3
|
import { RateLimiter } from "./rateLimiter";
|
|
5
4
|
import { RateLimitError } from "./RateLimitError";
|
|
@@ -24,6 +23,7 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
24
23
|
dbid?: string;
|
|
25
24
|
tableId?: string;
|
|
26
25
|
appId?: string;
|
|
26
|
+
startTime?: number; // Added for testing post-throttle timing
|
|
27
27
|
},
|
|
28
28
|
methodMap: { [P in keyof QuickbaseClient]: MethodInfo<P> },
|
|
29
29
|
baseHeaders: Record<string, string>,
|
|
@@ -31,10 +31,10 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
31
31
|
rateLimiter: RateLimiter,
|
|
32
32
|
transformDates: (obj: any, convertStringsToDates: boolean) => any,
|
|
33
33
|
debug: boolean | undefined,
|
|
34
|
-
convertDates: boolean
|
|
34
|
+
convertDates: boolean,
|
|
35
|
+
attempt: number = 0,
|
|
36
|
+
maxAttempts: number = rateLimiter.maxRetries + 1
|
|
35
37
|
): Promise<ReturnType<QuickbaseClient[K]>> {
|
|
36
|
-
console.log("[invokeMethod] Starting for method:", methodName);
|
|
37
|
-
|
|
38
38
|
const methodInfo = methodMap[methodName];
|
|
39
39
|
if (!methodInfo) throw new Error(`Method ${methodName} not found`);
|
|
40
40
|
|
|
@@ -42,9 +42,11 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
42
42
|
const body = hasBody ? params.body : undefined;
|
|
43
43
|
const restParams: any = hasBody
|
|
44
44
|
? Object.fromEntries(
|
|
45
|
-
Object.entries(params).filter(
|
|
45
|
+
Object.entries(params).filter(
|
|
46
|
+
([key]) => key !== "body" && key !== "startTime"
|
|
47
|
+
)
|
|
46
48
|
)
|
|
47
|
-
: { ...params };
|
|
49
|
+
: { ...params, startTime: undefined };
|
|
48
50
|
const requestParameters: any = {
|
|
49
51
|
...restParams,
|
|
50
52
|
...(hasBody ? { generated: body } : {}),
|
|
@@ -56,42 +58,27 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
56
58
|
};
|
|
57
59
|
|
|
58
60
|
let dbid: string | undefined = extractDbid(params);
|
|
59
|
-
console.log("[invokeMethod] Extracted dbid:", dbid);
|
|
60
|
-
|
|
61
61
|
let token = dbid
|
|
62
62
|
? await authStrategy.getToken(dbid)
|
|
63
63
|
: await authStrategy.getToken("");
|
|
64
|
-
console.log("[invokeMethod] Initial token:", token);
|
|
65
64
|
|
|
66
65
|
if (token) {
|
|
67
66
|
authStrategy.applyHeaders(baseHeaders, token);
|
|
68
67
|
requestOptions.headers = { ...baseHeaders };
|
|
69
|
-
console.log(
|
|
70
|
-
"[invokeMethod] Token found, headers set:",
|
|
71
|
-
requestOptions.headers
|
|
72
|
-
);
|
|
73
68
|
if (methodName === "getTempTokenDBID") {
|
|
74
|
-
console.log("[invokeMethod] Using cached token:", token);
|
|
75
69
|
return { temporaryAuthorization: token } as ReturnType<
|
|
76
70
|
QuickbaseClient[K]
|
|
77
71
|
>;
|
|
78
72
|
}
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
if (debug) {
|
|
82
|
-
console.log(`[${methodName}] requestParameters:`, requestParameters);
|
|
83
|
-
console.log(`[${methodName}] requestOptions:`, requestOptions);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
75
|
async function processResponse(
|
|
87
76
|
rawResponse: any
|
|
88
77
|
): Promise<ReturnType<QuickbaseClient[K]>> {
|
|
89
|
-
if (debug) console.log(`[${methodName}] rawResponse:`, rawResponse);
|
|
90
78
|
if (rawResponse instanceof Response) {
|
|
91
79
|
const contentType = rawResponse.headers
|
|
92
80
|
.get("Content-Type")
|
|
93
81
|
?.toLowerCase();
|
|
94
|
-
if (debug) console.log(`[${methodName}] contentType:`, contentType);
|
|
95
82
|
if (contentType?.includes("application/json")) {
|
|
96
83
|
const jsonResponse = await rawResponse.json();
|
|
97
84
|
return transformDates(jsonResponse, convertDates) as ReturnType<
|
|
@@ -102,8 +89,6 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
102
89
|
}
|
|
103
90
|
if (rawResponse && typeof rawResponse.value === "function") {
|
|
104
91
|
const response = await rawResponse.value();
|
|
105
|
-
if (debug)
|
|
106
|
-
console.log(`[${methodName}] Resolved JSONApiResponse:`, response);
|
|
107
92
|
return transformDates(response, convertDates) as ReturnType<
|
|
108
93
|
QuickbaseClient[K]
|
|
109
94
|
>;
|
|
@@ -140,36 +125,27 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
140
125
|
message = (await response.text()) || message;
|
|
141
126
|
}
|
|
142
127
|
} catch (e) {
|
|
143
|
-
if (debug) console.log(`[${methodName}] Error parsing response body:`, e);
|
|
144
128
|
message = "Failed to parse error response";
|
|
145
129
|
}
|
|
146
|
-
if (debug)
|
|
147
|
-
console.log(
|
|
148
|
-
"[invokeMethod] Parsed error - status:",
|
|
149
|
-
status,
|
|
150
|
-
"message:",
|
|
151
|
-
message
|
|
152
|
-
);
|
|
153
130
|
return { message, status };
|
|
154
131
|
}
|
|
155
132
|
|
|
156
|
-
let attempt = 0;
|
|
157
|
-
const maxAttempts = rateLimiter.maxRetries + 1;
|
|
158
|
-
|
|
159
133
|
while (attempt < maxAttempts) {
|
|
160
|
-
|
|
134
|
+
let acquired = false;
|
|
161
135
|
try {
|
|
162
136
|
await rateLimiter.throttle();
|
|
163
|
-
|
|
164
|
-
const
|
|
137
|
+
acquired = true;
|
|
138
|
+
const postThrottleTime = Date.now(); // Capture post-throttle time
|
|
139
|
+
if (params.startTime !== undefined) params.startTime = postThrottleTime; // Update startTime for testing
|
|
140
|
+
const responsePromise = methodInfo.method(
|
|
165
141
|
requestParameters,
|
|
166
142
|
requestOptions
|
|
167
143
|
);
|
|
168
|
-
|
|
144
|
+
rateLimiter.release(); // Release slot immediately after starting fetch
|
|
145
|
+
const response = await responsePromise;
|
|
169
146
|
return await processResponse(response);
|
|
170
147
|
} catch (error: unknown) {
|
|
171
|
-
if (
|
|
172
|
-
|
|
148
|
+
if (acquired) rateLimiter.release();
|
|
173
149
|
let status: number;
|
|
174
150
|
let message: string;
|
|
175
151
|
let response: Response | undefined;
|
|
@@ -181,29 +157,14 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
181
157
|
response = error;
|
|
182
158
|
({ message, status } = await parseErrorResponse(response));
|
|
183
159
|
} else {
|
|
184
|
-
|
|
185
|
-
// Attempt to handle as a fetch error with a response
|
|
186
|
-
if (error instanceof Error && "response" in error) {
|
|
187
|
-
response = (error as any).response;
|
|
188
|
-
if (response instanceof Response) {
|
|
189
|
-
({ message, status } = await parseErrorResponse(response));
|
|
190
|
-
} else {
|
|
191
|
-
throw error; // Rethrow if no response
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
throw error; // Rethrow truly unexpected errors
|
|
195
|
-
}
|
|
160
|
+
throw error;
|
|
196
161
|
}
|
|
197
162
|
|
|
198
|
-
if (debug)
|
|
199
|
-
console.log("[invokeMethod] Handling error with status:", status);
|
|
200
|
-
|
|
201
163
|
if (status === 429) {
|
|
202
164
|
if (!(error instanceof ResponseError)) {
|
|
203
165
|
throw new Error("Expected ResponseError for 429 handling");
|
|
204
166
|
}
|
|
205
167
|
const delay = await rateLimiter.handle429(error, attempt + 1);
|
|
206
|
-
if (debug) console.log(`[${methodName}] 429 delay: ${delay}ms`);
|
|
207
168
|
if (attempt + 1 === maxAttempts) {
|
|
208
169
|
throw new RateLimitError(
|
|
209
170
|
`API Error: ${message} (Status: ${status})`,
|
|
@@ -218,23 +179,29 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
|
|
|
218
179
|
continue;
|
|
219
180
|
}
|
|
220
181
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
182
|
+
let newToken: string | null;
|
|
183
|
+
try {
|
|
184
|
+
newToken = await authStrategy.handleError(
|
|
185
|
+
status,
|
|
186
|
+
params,
|
|
187
|
+
attempt,
|
|
188
|
+
maxAttempts,
|
|
189
|
+
debug,
|
|
190
|
+
methodName
|
|
191
|
+
);
|
|
192
|
+
} catch (authError) {
|
|
193
|
+
throw authError;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (newToken !== null) {
|
|
230
197
|
token = newToken;
|
|
231
198
|
authStrategy.applyHeaders(baseHeaders, token);
|
|
232
199
|
requestOptions.headers = { ...baseHeaders };
|
|
233
|
-
attempt++;
|
|
234
200
|
if (debug)
|
|
235
201
|
console.log(
|
|
236
202
|
`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`
|
|
237
203
|
);
|
|
204
|
+
attempt++;
|
|
238
205
|
continue;
|
|
239
206
|
}
|
|
240
207
|
|
package/src/quickbaseClient.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/quickbaseClient.ts
|
|
2
|
-
|
|
3
2
|
import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient";
|
|
4
3
|
import { Configuration, HTTPHeaders } from "./generated/runtime";
|
|
5
4
|
import * as apis from "./generated/apis";
|
|
@@ -9,7 +8,7 @@ import {
|
|
|
9
8
|
getParamNames,
|
|
10
9
|
transformDates,
|
|
11
10
|
extractHttpMethod,
|
|
12
|
-
} from "./utils";
|
|
11
|
+
} from "./utils";
|
|
13
12
|
import { invokeMethod, MethodInfo } from "./invokeMethod";
|
|
14
13
|
import {
|
|
15
14
|
TempTokenStrategy,
|
|
@@ -17,7 +16,7 @@ import {
|
|
|
17
16
|
AuthorizationStrategy,
|
|
18
17
|
SsoTokenStrategy,
|
|
19
18
|
} from "./authorizationStrategy";
|
|
20
|
-
import {
|
|
19
|
+
import { ConcurrentThrottleBucket } from "./ThrottleBucket";
|
|
21
20
|
import { RateLimiter } from "./rateLimiter";
|
|
22
21
|
|
|
23
22
|
export * from "./generated/models/index";
|
|
@@ -58,7 +57,7 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
58
57
|
debug,
|
|
59
58
|
convertDates = true,
|
|
60
59
|
tempTokenLifespan = 290000,
|
|
61
|
-
throttle = { rate:
|
|
60
|
+
throttle = { rate: 5, burst: 3 }, // Updated default to match passing test
|
|
62
61
|
maxRetries = 3,
|
|
63
62
|
retryDelay = 1000,
|
|
64
63
|
tokenCache: providedTokenCache,
|
|
@@ -67,7 +66,7 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
67
66
|
|
|
68
67
|
const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
|
|
69
68
|
const throttleBucket = throttle
|
|
70
|
-
? new
|
|
69
|
+
? new ConcurrentThrottleBucket(throttle.rate, throttle.burst)
|
|
71
70
|
: null;
|
|
72
71
|
const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
|
|
73
72
|
|
|
@@ -82,7 +81,13 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
const authStrategy: AuthorizationStrategy = useSso
|
|
85
|
-
? new SsoTokenStrategy(
|
|
84
|
+
? new SsoTokenStrategy(
|
|
85
|
+
samlToken || "",
|
|
86
|
+
realm,
|
|
87
|
+
effectiveFetch,
|
|
88
|
+
debug,
|
|
89
|
+
baseUrl
|
|
90
|
+
)
|
|
86
91
|
: useTempTokens
|
|
87
92
|
? new TempTokenStrategy(
|
|
88
93
|
tokenCache,
|
|
@@ -91,7 +96,7 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
91
96
|
realm,
|
|
92
97
|
baseUrl
|
|
93
98
|
)
|
|
94
|
-
: new UserTokenStrategy(userToken || "");
|
|
99
|
+
: new UserTokenStrategy(userToken || "", baseUrl);
|
|
95
100
|
|
|
96
101
|
const baseHeaders: HTTPHeaders = {
|
|
97
102
|
"QB-Realm-Hostname": `${realm}.quickbase.com`,
|
package/src/rateLimiter.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/rateLimiter.ts
|
|
2
|
-
import {
|
|
2
|
+
import { ConcurrentThrottleBucket } from "./ThrottleBucket"; // Updated import
|
|
3
3
|
import { ResponseError } from "./generated/runtime";
|
|
4
4
|
|
|
5
5
|
export class RateLimiter {
|
|
6
6
|
constructor(
|
|
7
|
-
private throttleBucket:
|
|
7
|
+
private throttleBucket: ConcurrentThrottleBucket | null, // Updated type
|
|
8
8
|
public maxRetries: number = 3,
|
|
9
9
|
private retryDelay: number = 1000
|
|
10
10
|
) {}
|
|
@@ -15,8 +15,13 @@ export class RateLimiter {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
release(): void {
|
|
19
|
+
if (this.throttleBucket) {
|
|
20
|
+
this.throttleBucket.release();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
async handle429(error: ResponseError, attempt: number): Promise<number> {
|
|
19
|
-
// Remove the throw here; let invokeMethod handle exhaustion
|
|
20
25
|
const retryAfter = error.response.headers.get("Retry-After");
|
|
21
26
|
return retryAfter
|
|
22
27
|
? parseInt(retryAfter, 10) * 1000
|