@cortexkit/anthropic-auth-core 1.19.1 → 1.21.0
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/README.md +1 -1
- package/dist/accounts.d.ts +81 -5
- package/dist/accounts.js +531 -81
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +3 -2
- package/dist/cache1h.d.ts +1 -1
- package/dist/cache1h.js +1 -1
- package/dist/cachekeep.d.ts +20 -2
- package/dist/cachekeep.js +134 -28
- package/dist/cch.d.ts +1 -0
- package/dist/cch.js +6 -1
- package/dist/claude-code.d.ts +5 -2
- package/dist/claude-code.js +127 -24
- package/dist/commands/account.js +1 -1
- package/dist/constants.d.ts +3 -3
- package/dist/dump.d.ts +13 -1
- package/dist/dump.js +171 -22
- package/dist/index.d.ts +3 -0
- package/dist/index.js +27 -24
- package/dist/killswitch.js +1 -1
- package/dist/models.d.ts +22 -6
- package/dist/models.js +16 -0
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/oauth-profile.js +10 -4
- package/dist/prime.d.ts +219 -0
- package/dist/prime.js +823 -0
- package/dist/quota-header-feed.d.ts +40 -0
- package/dist/quota-header-feed.js +248 -0
- package/dist/quota-headers.js +6 -4
- package/dist/quota-manager.d.ts +41 -25
- package/dist/quota-manager.js +391 -151
- package/dist/quotas.d.ts +4 -4
- package/dist/quotas.js +2 -2
- package/dist/relay.d.ts +2 -0
- package/dist/relay.js +10 -4
- package/dist/routing.d.ts +1 -1
- package/dist/routing.js +1 -1
- package/dist/start.d.ts +13 -0
- package/dist/start.js +29 -0
- package/dist/sticky-routing.d.ts +15 -2
- package/dist/sticky-routing.js +44 -17
- package/package.json +1 -1
package/dist/quota-manager.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* share one QuotaManager instance so they see the same in-memory cache.
|
|
6
6
|
* Handles deduplication, rate-limiting (429 backoff), and staleness.
|
|
7
7
|
*/
|
|
8
|
-
import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, quotaBackoffActive, } from
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, mergeMainQuotaErrorClearedAt, quotaBackoffActive, quotaSnapshotCheckedAt, } from './accounts.js';
|
|
9
|
+
import { logger } from './logger.js';
|
|
10
|
+
import { mergeHeaderQuotaSnapshot } from './quota-headers.js';
|
|
11
|
+
export { tokenFingerprint } from './token-fingerprint.js';
|
|
12
|
+
import { tokenFingerprint } from './token-fingerprint.js';
|
|
12
13
|
// Capture real setTimeout before tests can mock globalThis.setTimeout
|
|
13
14
|
const nativeSetTimeout = globalThis.setTimeout;
|
|
14
15
|
function mergePollCompletionWithNewerHeaders(current, polled) {
|
|
@@ -38,21 +39,23 @@ function mergePollCompletionWithNewerHeaders(current, polled) {
|
|
|
38
39
|
export class QuotaManager {
|
|
39
40
|
// --- State ---
|
|
40
41
|
main = null;
|
|
41
|
-
|
|
42
|
+
mainAccountId;
|
|
43
|
+
mainQuotaIdentityKnown = false;
|
|
44
|
+
mainQuotaIdentityStrict = false;
|
|
45
|
+
mainGeneration = 0;
|
|
46
|
+
mainQuotaErrorGeneration = 0;
|
|
47
|
+
mainQuotaErrorClearedAt;
|
|
42
48
|
fallbacks = new Map();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// stale entry instead of being treated as fresh.
|
|
46
|
-
fallbackTokenFps = new Map();
|
|
49
|
+
fallbackAuthLineages = new Map();
|
|
50
|
+
fallbackGenerations = new Map();
|
|
47
51
|
// --- Inflight deduplication ---
|
|
48
52
|
inflightMain = null;
|
|
49
|
-
|
|
53
|
+
inflightMainAccountId;
|
|
50
54
|
inflightFallbacks = new Map();
|
|
51
55
|
// --- Rate-limiting (scoped per route so a fallback 429 never backs off the
|
|
52
56
|
// main account or vice versa) ---
|
|
53
57
|
mainLastApiError = undefined;
|
|
54
58
|
fallbackApiErrors = new Map();
|
|
55
|
-
fallbackErrorTokenFps = new Map();
|
|
56
59
|
// --- Serial API gate (prevents concurrent quota API calls) ---
|
|
57
60
|
apiGate = Promise.resolve();
|
|
58
61
|
lastApiCallAt = 0;
|
|
@@ -68,49 +71,33 @@ export class QuotaManager {
|
|
|
68
71
|
this.now = opts.now ?? Date.now;
|
|
69
72
|
this.onMainQuotaFetched = opts.onMainQuotaFetched;
|
|
70
73
|
this.onApiError = opts.onApiError;
|
|
71
|
-
|
|
72
|
-
// that produced it. refreshMain() drops this seed if the live token's
|
|
73
|
-
// fingerprint differs (main-account switch), preventing stale wrong-account
|
|
74
|
-
// quota from being served during backoff.
|
|
75
|
-
this.seedMainFromStorage(opts.storage);
|
|
74
|
+
this.seedMainFromStorage(opts.storage, opts.storage?.mainAccountId);
|
|
76
75
|
this.seedMainBackoffFromStorage(opts.storage);
|
|
77
76
|
}
|
|
78
77
|
// =========================================================================
|
|
79
78
|
// Get (synchronous, from cache)
|
|
80
79
|
// =========================================================================
|
|
81
80
|
/**
|
|
82
|
-
* Cached main quota entry
|
|
83
|
-
*
|
|
84
|
-
* account switched), it is dropped and null is returned so the caller
|
|
85
|
-
* refetches for the current account. Called without a token (e.g. for
|
|
86
|
-
* display) it returns whatever is cached.
|
|
81
|
+
* Cached main quota entry, scoped to the account behind the main credential.
|
|
82
|
+
* An unresolved identity keeps a retained cache unverified rather than exposing it.
|
|
87
83
|
*/
|
|
88
|
-
getMain(
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
getMain(mainAccountId) {
|
|
85
|
+
if (this.mainAccountId !== mainAccountId)
|
|
86
|
+
return null;
|
|
87
|
+
if (this.mainQuotaIdentityKnown &&
|
|
88
|
+
this.mainQuotaIdentityStrict &&
|
|
89
|
+
(mainAccountId === undefined ||
|
|
90
|
+
this.main?.quota.accountIdentity !== mainAccountId)) {
|
|
91
|
+
return null;
|
|
95
92
|
}
|
|
96
93
|
return this.main;
|
|
97
94
|
}
|
|
98
95
|
/**
|
|
99
|
-
* Cached fallback quota entry
|
|
100
|
-
* binding: if the entry was produced by a different token (account re-login),
|
|
101
|
-
* it is dropped and null is returned so the caller refetches.
|
|
96
|
+
* Cached fallback quota entry, scoped to the configured account id.
|
|
102
97
|
*/
|
|
103
|
-
getFallback(accountId,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return entry;
|
|
107
|
-
const fp = this.fallbackTokenFps.get(accountId);
|
|
108
|
-
if (fp !== tokenFingerprint(accessToken)) {
|
|
109
|
-
this.fallbacks.delete(accountId);
|
|
110
|
-
this.fallbackTokenFps.delete(accountId);
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
return entry;
|
|
98
|
+
getFallback(accountId, account) {
|
|
99
|
+
this.bindFallbackLineage(accountId, account);
|
|
100
|
+
return this.fallbacks.get(accountId) ?? null;
|
|
114
101
|
}
|
|
115
102
|
getAllFallbacks() {
|
|
116
103
|
return this.fallbacks;
|
|
@@ -118,83 +105,154 @@ export class QuotaManager {
|
|
|
118
105
|
// =========================================================================
|
|
119
106
|
// Set (manual inject — seeding from persisted account.quota on boot)
|
|
120
107
|
// =========================================================================
|
|
121
|
-
setMain(
|
|
122
|
-
this.
|
|
108
|
+
setMain(mainAccountId, entry) {
|
|
109
|
+
this.mainAccountId = mainAccountId;
|
|
123
110
|
this.main = entry;
|
|
124
111
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
112
|
+
/** Bind quota state to the account behind the current main credential. */
|
|
113
|
+
setMainQuotaAccountIdentity(accountIdentity, strict = true) {
|
|
114
|
+
const previousIdentityKnown = this.mainQuotaIdentityKnown;
|
|
115
|
+
const previousAccountIdentity = this.mainAccountId;
|
|
116
|
+
const changed = !previousIdentityKnown ||
|
|
117
|
+
this.mainAccountId !== accountIdentity ||
|
|
118
|
+
this.mainQuotaIdentityStrict !== strict;
|
|
119
|
+
this.mainQuotaIdentityKnown = true;
|
|
120
|
+
this.mainQuotaIdentityStrict = strict;
|
|
121
|
+
if (!changed)
|
|
122
|
+
return undefined;
|
|
123
|
+
this.mainGeneration += 1;
|
|
124
|
+
this.mainAccountId = accountIdentity;
|
|
125
|
+
if (accountIdentity !== undefined &&
|
|
126
|
+
this.main?.quota.accountIdentity !== accountIdentity) {
|
|
127
|
+
this.main = null;
|
|
129
128
|
}
|
|
130
|
-
|
|
131
|
-
this.
|
|
129
|
+
if (accountIdentity !== undefined &&
|
|
130
|
+
this.mainLastApiError &&
|
|
131
|
+
(this.mainLastApiError.accountIdentity !== undefined
|
|
132
|
+
? this.mainLastApiError.accountIdentity !== accountIdentity
|
|
133
|
+
: previousIdentityKnown &&
|
|
134
|
+
previousAccountIdentity !== undefined &&
|
|
135
|
+
previousAccountIdentity !== accountIdentity)) {
|
|
136
|
+
this.mainLastApiError = undefined;
|
|
137
|
+
this.mainQuotaErrorGeneration += 1;
|
|
138
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, this.now());
|
|
139
|
+
return this.mainQuotaErrorGeneration;
|
|
132
140
|
}
|
|
141
|
+
return undefined;
|
|
133
142
|
}
|
|
134
|
-
|
|
143
|
+
setFallback(accountId, entry, account) {
|
|
144
|
+
this.bindFallbackLineage(accountId, account);
|
|
145
|
+
this.fallbacks.set(accountId, entry);
|
|
146
|
+
}
|
|
147
|
+
clearFallback(accountId) {
|
|
148
|
+
this.fallbackGenerations.set(accountId, (this.fallbackGenerations.get(accountId) ?? 0) + 1);
|
|
149
|
+
this.fallbacks.delete(accountId);
|
|
150
|
+
this.fallbackApiErrors.delete(accountId);
|
|
151
|
+
this.inflightFallbacks.delete(QuotaManager.fallbackInflightKey(accountId));
|
|
152
|
+
}
|
|
153
|
+
pushMainFromHeaders(mainAccountId, incoming) {
|
|
135
154
|
const checkedAt = incoming.checkedAt ?? this.now();
|
|
136
|
-
const
|
|
137
|
-
|
|
155
|
+
const quota = mergeHeaderQuotaSnapshot(this.mainAccountId === mainAccountId &&
|
|
156
|
+
(!this.mainQuotaIdentityKnown ||
|
|
157
|
+
!this.mainQuotaIdentityStrict ||
|
|
158
|
+
this.main?.quota.accountIdentity === mainAccountId)
|
|
159
|
+
? this.main?.quota
|
|
160
|
+
: undefined, {
|
|
161
|
+
...incoming,
|
|
162
|
+
...(mainAccountId !== undefined && { accountIdentity: mainAccountId }),
|
|
163
|
+
});
|
|
138
164
|
const entry = {
|
|
139
165
|
quota,
|
|
140
166
|
checkedAt,
|
|
141
167
|
refreshAfter: getQuotaNextRefreshAt(quota, this.storage, checkedAt),
|
|
142
168
|
};
|
|
143
|
-
this.setMain(
|
|
169
|
+
this.setMain(mainAccountId, entry);
|
|
144
170
|
return entry;
|
|
145
171
|
}
|
|
146
|
-
pushFallbackFromHeaders(accountId,
|
|
172
|
+
pushFallbackFromHeaders(accountId, incoming, account) {
|
|
173
|
+
if (!this.acceptFallbackQuotaObservation(accountId, account))
|
|
174
|
+
return null;
|
|
147
175
|
const checkedAt = incoming.checkedAt ?? this.now();
|
|
148
|
-
const quota = mergeHeaderQuotaSnapshot(this.
|
|
176
|
+
const quota = mergeHeaderQuotaSnapshot(this.fallbacks.get(accountId)?.quota, {
|
|
177
|
+
...incoming,
|
|
178
|
+
accountIdentity: accountId,
|
|
179
|
+
});
|
|
149
180
|
const entry = {
|
|
150
181
|
quota,
|
|
151
182
|
checkedAt,
|
|
152
183
|
refreshAfter: getQuotaNextRefreshAt(quota, this.storage, checkedAt),
|
|
153
184
|
};
|
|
154
|
-
this.
|
|
185
|
+
this.fallbacks.set(accountId, entry);
|
|
155
186
|
return entry;
|
|
156
187
|
}
|
|
157
188
|
// =========================================================================
|
|
158
189
|
// Refresh (async, deduplicated, rate-limited)
|
|
159
190
|
// =========================================================================
|
|
160
|
-
async refreshMain(accessToken) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
async refreshMain(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
|
|
192
|
+
const mainAccountId = mainAccountIdOrAccessToken;
|
|
193
|
+
const credential = accessToken ?? mainAccountIdOrAccessToken;
|
|
194
|
+
if (!credential)
|
|
195
|
+
throw new Error('Main OAuth access token is unavailable');
|
|
196
|
+
return (await this.refreshMainWithMetadata(mainAccountId, credential, expectedIdentityGeneration)).quota;
|
|
197
|
+
}
|
|
198
|
+
async refreshMainWithMetadata(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
|
|
199
|
+
const effectiveAccountId = mainAccountIdOrAccessToken;
|
|
200
|
+
const credential = accessToken ?? mainAccountIdOrAccessToken;
|
|
201
|
+
if (!credential)
|
|
202
|
+
throw new Error('Main OAuth access token is unavailable');
|
|
203
|
+
if (expectedIdentityGeneration !== undefined &&
|
|
204
|
+
(this.mainGeneration !== expectedIdentityGeneration ||
|
|
205
|
+
this.mainAccountId !== effectiveAccountId)) {
|
|
206
|
+
throw new Error('Main quota identity changed before refresh');
|
|
207
|
+
}
|
|
208
|
+
if (this.mainAccountId !== effectiveAccountId) {
|
|
166
209
|
this.main = null;
|
|
167
|
-
this.
|
|
210
|
+
this.mainAccountId = effectiveAccountId;
|
|
211
|
+
if (this.mainQuotaIdentityKnown)
|
|
212
|
+
this.mainGeneration += 1;
|
|
168
213
|
}
|
|
169
|
-
|
|
170
|
-
if (this.inflightMain && this.inflightMainFp === fp)
|
|
214
|
+
if (this.inflightMain && this.inflightMainAccountId === effectiveAccountId)
|
|
171
215
|
return this.inflightMain;
|
|
172
216
|
// Rate-limit — if API recently 429'd, return stale or throw
|
|
173
217
|
if (this.isBackedOff()) {
|
|
174
|
-
if (this.main)
|
|
175
|
-
return this.main.quota;
|
|
218
|
+
if (this.main && this.mainAccountId === effectiveAccountId) {
|
|
219
|
+
return { quota: this.main.quota, fetched: false };
|
|
220
|
+
}
|
|
176
221
|
throw new Error('Quota API rate-limited — try again later');
|
|
177
222
|
}
|
|
178
|
-
this.
|
|
179
|
-
|
|
223
|
+
this.inflightMainAccountId = effectiveAccountId;
|
|
224
|
+
const generation = this.mainGeneration;
|
|
225
|
+
this.inflightMain = this._fetchMain(effectiveAccountId, credential, generation);
|
|
180
226
|
return this.inflightMain;
|
|
181
227
|
}
|
|
182
|
-
async refreshFallback(accountId, accessToken) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
228
|
+
async refreshFallback(accountId, accessToken, account) {
|
|
229
|
+
return (await this.refreshFallbackWithMetadata(accountId, accessToken, account)).quota;
|
|
230
|
+
}
|
|
231
|
+
async refreshFallbackWithMetadata(accountId, accessToken, account) {
|
|
232
|
+
this.bindFallbackLineage(accountId, account);
|
|
233
|
+
const inflightKey = QuotaManager.fallbackInflightKey(accountId);
|
|
186
234
|
const inflight = this.inflightFallbacks.get(inflightKey);
|
|
187
235
|
if (inflight)
|
|
188
236
|
return inflight;
|
|
189
237
|
// Rate-limit — scoped to THIS fallback account only
|
|
190
|
-
if (this.isFallbackBackedOff(accountId
|
|
191
|
-
const cached = this.getFallback(accountId
|
|
238
|
+
if (this.isFallbackBackedOff(accountId)) {
|
|
239
|
+
const cached = this.getFallback(accountId);
|
|
192
240
|
if (cached)
|
|
193
|
-
return cached.quota;
|
|
241
|
+
return { quota: cached.quota, fetched: false };
|
|
194
242
|
throw new Error('Quota API rate-limited — try again later');
|
|
195
243
|
}
|
|
196
|
-
const
|
|
244
|
+
const generation = this.fallbackGenerations.get(accountId) ?? 0;
|
|
245
|
+
const promise = this._fetchFallback(accountId, accessToken, generation, account);
|
|
197
246
|
this.inflightFallbacks.set(inflightKey, promise);
|
|
247
|
+
void promise.then(() => {
|
|
248
|
+
if (this.inflightFallbacks.get(inflightKey) === promise) {
|
|
249
|
+
this.inflightFallbacks.delete(inflightKey);
|
|
250
|
+
}
|
|
251
|
+
}, () => {
|
|
252
|
+
if (this.inflightFallbacks.get(inflightKey) === promise) {
|
|
253
|
+
this.inflightFallbacks.delete(inflightKey);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
198
256
|
return promise;
|
|
199
257
|
}
|
|
200
258
|
async refreshAllFallbacks(accounts) {
|
|
@@ -204,11 +262,12 @@ export class QuotaManager {
|
|
|
204
262
|
continue;
|
|
205
263
|
if (!account.access)
|
|
206
264
|
continue;
|
|
207
|
-
|
|
265
|
+
this.bindFallbackLineage(account.id, account);
|
|
266
|
+
const cached = this.getFallback(account.id);
|
|
208
267
|
if (cached && now < cached.refreshAfter)
|
|
209
268
|
continue;
|
|
210
269
|
try {
|
|
211
|
-
await this.refreshFallback(account.id, account.access);
|
|
270
|
+
await this.refreshFallback(account.id, account.access, account);
|
|
212
271
|
}
|
|
213
272
|
catch {
|
|
214
273
|
// Best-effort — keep stale cache entry if fetch fails
|
|
@@ -223,7 +282,7 @@ export class QuotaManager {
|
|
|
223
282
|
return;
|
|
224
283
|
if (this.isBackedOff())
|
|
225
284
|
return;
|
|
226
|
-
void this.refreshMain(accessToken).catch(() => { });
|
|
285
|
+
void this.refreshMain(this.mainAccountId, accessToken).catch(() => { });
|
|
227
286
|
}
|
|
228
287
|
// =========================================================================
|
|
229
288
|
// Staleness queries
|
|
@@ -236,12 +295,19 @@ export class QuotaManager {
|
|
|
236
295
|
isMainStale(modelId) {
|
|
237
296
|
if (!this.main)
|
|
238
297
|
return true;
|
|
298
|
+
if (this.mainQuotaIdentityKnown &&
|
|
299
|
+
this.mainQuotaIdentityStrict &&
|
|
300
|
+
(this.mainAccountId === undefined ||
|
|
301
|
+
this.main.quota.accountIdentity !== this.mainAccountId)) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
239
304
|
return (this.now() >= this.main.refreshAfter ||
|
|
240
305
|
this.scopedWindowIsStale(this.main, modelId));
|
|
241
306
|
}
|
|
242
|
-
isFallbackStale(accountId,
|
|
243
|
-
//
|
|
244
|
-
|
|
307
|
+
isFallbackStale(accountId, _accessToken, modelId, account) {
|
|
308
|
+
// Keep the unused slot to avoid breaking internal callers that pass modelId
|
|
309
|
+
// third; cached quota is scoped to stable account identity.
|
|
310
|
+
const entry = this.getFallback(accountId, account);
|
|
245
311
|
if (!entry)
|
|
246
312
|
return true;
|
|
247
313
|
return (this.now() >= entry.refreshAfter ||
|
|
@@ -266,7 +332,7 @@ export class QuotaManager {
|
|
|
266
332
|
// =========================================================================
|
|
267
333
|
updateStorage(storage) {
|
|
268
334
|
this.storage = storage;
|
|
269
|
-
this.seedMainFromStorage(storage);
|
|
335
|
+
this.seedMainFromStorage(storage, this.mainQuotaIdentityKnown ? this.mainAccountId : storage?.mainAccountId);
|
|
270
336
|
this.seedMainBackoffFromStorage(storage);
|
|
271
337
|
}
|
|
272
338
|
/**
|
|
@@ -275,37 +341,106 @@ export class QuotaManager {
|
|
|
275
341
|
* can stop this process from showing "checking…" or making a redundant quota
|
|
276
342
|
* API call.
|
|
277
343
|
*/
|
|
278
|
-
seedMainFromStorage(storage,
|
|
344
|
+
seedMainFromStorage(storage, mainAccountId) {
|
|
279
345
|
const persisted = getPersistedMainQuota(storage);
|
|
280
346
|
if (!persisted)
|
|
281
347
|
return;
|
|
282
|
-
const accessTokenFp = accessToken ? tokenFingerprint(accessToken) : null;
|
|
283
|
-
if (accessTokenFp &&
|
|
284
|
-
persisted.tokenFingerprint &&
|
|
285
|
-
persisted.tokenFingerprint !== accessTokenFp) {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
348
|
const entry = {
|
|
289
|
-
quota:
|
|
349
|
+
quota: {
|
|
350
|
+
...persisted.quota,
|
|
351
|
+
...((!this.mainQuotaIdentityKnown || !this.mainQuotaIdentityStrict) &&
|
|
352
|
+
mainAccountId !== undefined &&
|
|
353
|
+
persisted.accountIdentity === undefined && {
|
|
354
|
+
accountIdentity: mainAccountId,
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
290
357
|
refreshAfter: getQuotaNextRefreshAt(persisted.quota, storage, persisted.checkedAt),
|
|
291
358
|
checkedAt: persisted.checkedAt,
|
|
292
359
|
};
|
|
360
|
+
const current = this.main &&
|
|
361
|
+
this.mainAccountId === mainAccountId &&
|
|
362
|
+
(!this.mainQuotaIdentityKnown ||
|
|
363
|
+
!this.mainQuotaIdentityStrict ||
|
|
364
|
+
this.main.quota.accountIdentity === mainAccountId)
|
|
365
|
+
? this.main
|
|
366
|
+
: undefined;
|
|
367
|
+
if (current &&
|
|
368
|
+
current.checkedAt >= entry.checkedAt &&
|
|
369
|
+
current.quota.source === 'headers' &&
|
|
370
|
+
entry.quota.source === 'poll') {
|
|
371
|
+
const mergedQuota = mergeHeaderQuotaSnapshot(entry.quota, current.quota);
|
|
372
|
+
this.main = {
|
|
373
|
+
quota: mergedQuota,
|
|
374
|
+
refreshAfter: getQuotaNextRefreshAt(mergedQuota, storage, current.checkedAt),
|
|
375
|
+
checkedAt: current.checkedAt,
|
|
376
|
+
};
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
293
379
|
if (this.main &&
|
|
294
|
-
this.
|
|
295
|
-
|
|
296
|
-
!this.mainTokenFp ||
|
|
297
|
-
this.mainTokenFp === accessTokenFp)) {
|
|
380
|
+
this.mainAccountId === mainAccountId &&
|
|
381
|
+
this.main.checkedAt >= entry.checkedAt) {
|
|
298
382
|
return;
|
|
299
383
|
}
|
|
300
384
|
this.main = entry;
|
|
301
|
-
|
|
385
|
+
if (mainAccountId !== undefined)
|
|
386
|
+
this.mainAccountId = mainAccountId;
|
|
302
387
|
}
|
|
303
388
|
seedMainBackoffFromStorage(storage) {
|
|
304
389
|
const persistedError = storage?.quota?.mainLastQuotaApiError;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
390
|
+
const persistedIdentity = persistedError?.accountIdentity;
|
|
391
|
+
const persistedGeneration = typeof storage?.quota?.mainQuotaErrorGeneration === 'number' &&
|
|
392
|
+
Number.isSafeInteger(storage.quota.mainQuotaErrorGeneration) &&
|
|
393
|
+
storage.quota.mainQuotaErrorGeneration >= 0
|
|
394
|
+
? storage.quota.mainQuotaErrorGeneration
|
|
395
|
+
: undefined;
|
|
396
|
+
const persistedClearedAt = typeof storage?.quota?.mainQuotaErrorClearedAt === 'number' &&
|
|
397
|
+
Number.isFinite(storage.quota.mainQuotaErrorClearedAt) &&
|
|
398
|
+
storage.quota.mainQuotaErrorClearedAt >= 0
|
|
399
|
+
? storage.quota.mainQuotaErrorClearedAt
|
|
400
|
+
: undefined;
|
|
401
|
+
if (persistedClearedAt !== undefined &&
|
|
402
|
+
(this.mainQuotaErrorClearedAt === undefined ||
|
|
403
|
+
persistedClearedAt > this.mainQuotaErrorClearedAt)) {
|
|
404
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, persistedClearedAt);
|
|
405
|
+
if ((!persistedError ||
|
|
406
|
+
(typeof persistedError.checkedAt === 'number' &&
|
|
407
|
+
persistedError.checkedAt <= persistedClearedAt)) &&
|
|
408
|
+
(this.mainLastApiError?.checkedAt === undefined ||
|
|
409
|
+
this.mainLastApiError.checkedAt <= persistedClearedAt)) {
|
|
410
|
+
this.mainLastApiError = undefined;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (persistedGeneration !== undefined &&
|
|
414
|
+
persistedGeneration > this.mainQuotaErrorGeneration) {
|
|
415
|
+
this.mainQuotaErrorGeneration = persistedGeneration;
|
|
416
|
+
if (!persistedError &&
|
|
417
|
+
(this.mainLastApiError?.checkedAt === undefined ||
|
|
418
|
+
this.mainQuotaErrorClearedAt === undefined ||
|
|
419
|
+
this.mainLastApiError.checkedAt <= this.mainQuotaErrorClearedAt)) {
|
|
420
|
+
this.mainLastApiError = undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (persistedError &&
|
|
424
|
+
(!this.mainQuotaIdentityKnown ||
|
|
425
|
+
persistedIdentity === undefined ||
|
|
426
|
+
persistedIdentity === this.mainAccountId) &&
|
|
427
|
+
(this.mainQuotaErrorClearedAt === undefined ||
|
|
428
|
+
typeof persistedError.checkedAt !== 'number' ||
|
|
429
|
+
persistedError.checkedAt > this.mainQuotaErrorClearedAt) &&
|
|
430
|
+
(this.mainLastApiError?.checkedAt === undefined ||
|
|
431
|
+
persistedError.checkedAt >= this.mainLastApiError.checkedAt) &&
|
|
432
|
+
quotaBackoffActive(persistedError, this.now())) {
|
|
433
|
+
this.mainLastApiError = persistedError;
|
|
434
|
+
if (persistedGeneration !== undefined) {
|
|
435
|
+
this.mainQuotaErrorGeneration = persistedGeneration;
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (this.mainAccountId !== undefined &&
|
|
440
|
+
this.mainLastApiError?.accountIdentity !== undefined &&
|
|
441
|
+
this.mainLastApiError.accountIdentity !== this.mainAccountId) {
|
|
442
|
+
this.mainLastApiError = undefined;
|
|
443
|
+
}
|
|
309
444
|
}
|
|
310
445
|
/**
|
|
311
446
|
* Seed fallback cache entries from persisted account.quota data.
|
|
@@ -314,22 +449,32 @@ export class QuotaManager {
|
|
|
314
449
|
*/
|
|
315
450
|
seedFallbacksFromAccounts(accounts) {
|
|
316
451
|
const checkInterval = getQuotaCheckIntervalMs(this.storage);
|
|
452
|
+
const configuredIds = new Set(accounts.map((account) => account.id));
|
|
453
|
+
for (const accountId of this.fallbackAuthLineages.keys()) {
|
|
454
|
+
if (!configuredIds.has(accountId)) {
|
|
455
|
+
this.clearFallback(accountId);
|
|
456
|
+
this.fallbackAuthLineages.delete(accountId);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
317
459
|
for (const account of accounts) {
|
|
460
|
+
const lineageChanged = this.bindFallbackLineage(account.id, account);
|
|
461
|
+
if (lineageChanged)
|
|
462
|
+
continue;
|
|
318
463
|
if (account.enabled === false)
|
|
319
464
|
continue;
|
|
320
465
|
if (!account.quota)
|
|
321
466
|
continue;
|
|
322
|
-
const checkedAt =
|
|
467
|
+
const checkedAt = quotaSnapshotCheckedAt(account.quota);
|
|
323
468
|
if (checkedAt <= 0)
|
|
324
469
|
continue;
|
|
325
|
-
const existing = this.getFallback(account.id
|
|
470
|
+
const existing = this.getFallback(account.id);
|
|
326
471
|
if (existing && existing.checkedAt >= checkedAt)
|
|
327
472
|
continue;
|
|
328
473
|
this.setFallback(account.id, {
|
|
329
|
-
quota: account.quota,
|
|
474
|
+
quota: { ...account.quota, accountIdentity: account.id },
|
|
330
475
|
refreshAfter: checkedAt + checkInterval,
|
|
331
476
|
checkedAt,
|
|
332
|
-
}, account
|
|
477
|
+
}, account);
|
|
333
478
|
}
|
|
334
479
|
}
|
|
335
480
|
/**
|
|
@@ -337,34 +482,77 @@ export class QuotaManager {
|
|
|
337
482
|
* account — a fallback account's 429 never reports here.
|
|
338
483
|
*/
|
|
339
484
|
isBackedOff() {
|
|
485
|
+
// Unlike cached numbers above, restrictions stay active while identity is
|
|
486
|
+
// ambiguous; only a confirmed account change may release an inherited guard.
|
|
340
487
|
return quotaBackoffActive(this.mainLastApiError, this.now());
|
|
341
488
|
}
|
|
342
489
|
/**
|
|
343
490
|
* Whether a specific fallback account's quota API is in backoff.
|
|
344
491
|
*/
|
|
345
|
-
isFallbackBackedOff(accountId
|
|
346
|
-
if (accessToken) {
|
|
347
|
-
const errorFp = this.fallbackErrorTokenFps.get(accountId);
|
|
348
|
-
if (errorFp !== tokenFingerprint(accessToken))
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
492
|
+
isFallbackBackedOff(accountId) {
|
|
351
493
|
return quotaBackoffActive(this.fallbackApiErrors.get(accountId), this.now());
|
|
352
494
|
}
|
|
353
495
|
getLastApiError() {
|
|
354
496
|
return this.mainLastApiError;
|
|
355
497
|
}
|
|
498
|
+
getMainQuotaErrorGeneration() {
|
|
499
|
+
return this.mainQuotaErrorGeneration;
|
|
500
|
+
}
|
|
501
|
+
getMainQuotaIdentityGeneration() {
|
|
502
|
+
return this.mainGeneration;
|
|
503
|
+
}
|
|
504
|
+
getMainQuotaErrorClearedAt() {
|
|
505
|
+
return this.mainQuotaErrorClearedAt;
|
|
506
|
+
}
|
|
507
|
+
clearMainBackoff() {
|
|
508
|
+
if (!this.mainLastApiError)
|
|
509
|
+
return undefined;
|
|
510
|
+
this.mainLastApiError = undefined;
|
|
511
|
+
this.mainQuotaErrorGeneration += 1;
|
|
512
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, this.now());
|
|
513
|
+
return this.mainQuotaErrorGeneration;
|
|
514
|
+
}
|
|
356
515
|
// =========================================================================
|
|
357
516
|
// Private
|
|
358
517
|
// =========================================================================
|
|
359
518
|
/** Minimum gap between consecutive quota API calls (ms). */
|
|
360
519
|
static API_CALL_GAP_MS = 1_000;
|
|
361
|
-
static fallbackInflightKey(accountId
|
|
362
|
-
return
|
|
520
|
+
static fallbackInflightKey(accountId) {
|
|
521
|
+
return accountId;
|
|
363
522
|
}
|
|
364
523
|
static quotaLockName(accountId) {
|
|
365
524
|
const safeId = accountId.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
366
525
|
return `opencode-fallback-quota-refresh-${safeId || 'account'}`;
|
|
367
526
|
}
|
|
527
|
+
bindFallbackLineage(accountId, account) {
|
|
528
|
+
if (!account)
|
|
529
|
+
return false;
|
|
530
|
+
const hadLineage = this.fallbackAuthLineages.has(accountId);
|
|
531
|
+
const previousLineage = this.fallbackAuthLineages.get(accountId);
|
|
532
|
+
const lineageChanged = hadLineage &&
|
|
533
|
+
account.authLineageId !== undefined &&
|
|
534
|
+
previousLineage !== account.authLineageId;
|
|
535
|
+
if (lineageChanged)
|
|
536
|
+
this.clearFallback(accountId);
|
|
537
|
+
this.fallbackAuthLineages.set(accountId, account.authLineageId);
|
|
538
|
+
return lineageChanged;
|
|
539
|
+
}
|
|
540
|
+
acceptFallbackQuotaObservation(accountId, account) {
|
|
541
|
+
const hasLineage = this.fallbackAuthLineages.has(accountId);
|
|
542
|
+
const currentLineage = this.fallbackAuthLineages.get(accountId);
|
|
543
|
+
const observedLineage = account?.authLineageId;
|
|
544
|
+
if (hasLineage && currentLineage !== observedLineage) {
|
|
545
|
+
logger.trace('quota', 'skipped stale fallback quota observation after lineage change', {
|
|
546
|
+
accountId,
|
|
547
|
+
storedLineage: currentLineage,
|
|
548
|
+
servedLineage: observedLineage,
|
|
549
|
+
});
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
if (!hasLineage)
|
|
553
|
+
this.fallbackAuthLineages.set(accountId, observedLineage);
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
368
556
|
/**
|
|
369
557
|
* Serialize API calls through a shared gate so only one
|
|
370
558
|
* quota API request runs at a time, with a minimum gap
|
|
@@ -389,25 +577,36 @@ export class QuotaManager {
|
|
|
389
577
|
this.apiGate = queued.catch(() => { });
|
|
390
578
|
return queued;
|
|
391
579
|
}
|
|
392
|
-
async _fetchMain(accessToken) {
|
|
393
|
-
const thisFetchFp = tokenFingerprint(accessToken);
|
|
580
|
+
async _fetchMain(mainAccountId, accessToken, generation) {
|
|
394
581
|
return this._enqueueApiFetch(async () => {
|
|
395
582
|
try {
|
|
396
583
|
// Re-check backoff inside gate — may have been set by
|
|
397
584
|
// a preceding queued call while we waited
|
|
398
585
|
if (this.isBackedOff()) {
|
|
399
|
-
if (this.main)
|
|
400
|
-
return this.main.quota;
|
|
586
|
+
if (this.main && this.mainAccountId === mainAccountId) {
|
|
587
|
+
return { quota: this.main.quota, fetched: false };
|
|
588
|
+
}
|
|
401
589
|
throw new Error('Quota API rate-limited — try again later');
|
|
402
590
|
}
|
|
591
|
+
if (this.mainQuotaIdentityKnown &&
|
|
592
|
+
this.mainQuotaIdentityStrict &&
|
|
593
|
+
this.mainGeneration !== generation) {
|
|
594
|
+
return {
|
|
595
|
+
quota: { accountIdentity: mainAccountId },
|
|
596
|
+
fetched: false,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
403
599
|
const fileLock = await acquireRefreshFileLock({
|
|
404
600
|
name: 'opencode-main-quota-refresh',
|
|
405
601
|
ttlMs: 30_000,
|
|
406
602
|
});
|
|
407
603
|
if (!fileLock) {
|
|
408
604
|
const cached = this.main;
|
|
409
|
-
if (cached &&
|
|
410
|
-
|
|
605
|
+
if (cached &&
|
|
606
|
+
this.mainAccountId === mainAccountId &&
|
|
607
|
+
this.now() < cached.refreshAfter) {
|
|
608
|
+
return { quota: cached.quota, fetched: false };
|
|
609
|
+
}
|
|
411
610
|
throw new Error('Quota refresh is already in progress');
|
|
412
611
|
}
|
|
413
612
|
try {
|
|
@@ -418,19 +617,46 @@ export class QuotaManager {
|
|
|
418
617
|
now: this.now,
|
|
419
618
|
});
|
|
420
619
|
const now = this.now();
|
|
421
|
-
|
|
422
|
-
|
|
620
|
+
if (this.mainQuotaIdentityKnown &&
|
|
621
|
+
this.mainQuotaIdentityStrict &&
|
|
622
|
+
this.mainGeneration !== generation) {
|
|
623
|
+
return {
|
|
624
|
+
quota: {
|
|
625
|
+
...quota,
|
|
626
|
+
...(mainAccountId !== undefined && {
|
|
627
|
+
accountIdentity: mainAccountId,
|
|
628
|
+
}),
|
|
629
|
+
},
|
|
630
|
+
fetched: true,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const completedQuota = mergePollCompletionWithNewerHeaders(this.mainAccountId === mainAccountId &&
|
|
634
|
+
(!this.mainQuotaIdentityKnown ||
|
|
635
|
+
!this.mainQuotaIdentityStrict ||
|
|
636
|
+
this.main?.quota.accountIdentity === mainAccountId)
|
|
637
|
+
? this.main?.quota
|
|
638
|
+
: undefined, {
|
|
639
|
+
...quota,
|
|
640
|
+
...(mainAccountId !== undefined && {
|
|
641
|
+
accountIdentity: mainAccountId,
|
|
642
|
+
}),
|
|
643
|
+
});
|
|
644
|
+
this.mainAccountId = mainAccountId;
|
|
423
645
|
this.main = {
|
|
424
646
|
quota: completedQuota,
|
|
425
647
|
refreshAfter: getQuotaNextRefreshAt(completedQuota, this.storage, now),
|
|
426
648
|
checkedAt: completedQuota.checkedAt ?? now,
|
|
427
649
|
};
|
|
428
|
-
this.mainLastApiError
|
|
429
|
-
|
|
430
|
-
|
|
650
|
+
if (this.mainLastApiError) {
|
|
651
|
+
this.mainLastApiError = undefined;
|
|
652
|
+
this.mainQuotaErrorGeneration += 1;
|
|
653
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, now);
|
|
654
|
+
}
|
|
655
|
+
this.onMainQuotaFetched?.(completedQuota, now, tokenFingerprint(accessToken), fetchStartedAt, this.mainQuotaErrorGeneration, this.mainQuotaErrorClearedAt);
|
|
656
|
+
return { quota: completedQuota, fetched: true };
|
|
431
657
|
}
|
|
432
658
|
catch (error) {
|
|
433
|
-
this._handleMainFetchError(error);
|
|
659
|
+
this._handleMainFetchError(error, generation);
|
|
434
660
|
throw error;
|
|
435
661
|
}
|
|
436
662
|
finally {
|
|
@@ -438,21 +664,24 @@ export class QuotaManager {
|
|
|
438
664
|
}
|
|
439
665
|
}
|
|
440
666
|
finally {
|
|
441
|
-
if (this.
|
|
667
|
+
if (this.inflightMainAccountId === mainAccountId) {
|
|
442
668
|
this.inflightMain = null;
|
|
443
|
-
this.
|
|
669
|
+
this.inflightMainAccountId = undefined;
|
|
444
670
|
}
|
|
445
671
|
}
|
|
446
672
|
});
|
|
447
673
|
}
|
|
448
|
-
async _fetchFallback(accountId, accessToken) {
|
|
674
|
+
async _fetchFallback(accountId, accessToken, generation, account) {
|
|
449
675
|
return this._enqueueApiFetch(async () => {
|
|
450
676
|
try {
|
|
677
|
+
if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation) {
|
|
678
|
+
return { quota: { accountIdentity: accountId }, fetched: false };
|
|
679
|
+
}
|
|
451
680
|
// Re-check backoff inside gate — scoped to this fallback account
|
|
452
|
-
if (this.isFallbackBackedOff(accountId
|
|
453
|
-
const cached = this.getFallback(accountId,
|
|
681
|
+
if (this.isFallbackBackedOff(accountId)) {
|
|
682
|
+
const cached = this.getFallback(accountId, account);
|
|
454
683
|
if (cached)
|
|
455
|
-
return cached.quota;
|
|
684
|
+
return { quota: cached.quota, fetched: false };
|
|
456
685
|
throw new Error('Quota API rate-limited — try again later');
|
|
457
686
|
}
|
|
458
687
|
const fileLock = await acquireRefreshFileLock({
|
|
@@ -460,9 +689,10 @@ export class QuotaManager {
|
|
|
460
689
|
ttlMs: 30_000,
|
|
461
690
|
});
|
|
462
691
|
if (!fileLock) {
|
|
463
|
-
const cached = this.getFallback(accountId,
|
|
464
|
-
if (cached && this.now() < cached.refreshAfter)
|
|
465
|
-
return cached.quota;
|
|
692
|
+
const cached = this.getFallback(accountId, account);
|
|
693
|
+
if (cached && this.now() < cached.refreshAfter) {
|
|
694
|
+
return { quota: cached.quota, fetched: false };
|
|
695
|
+
}
|
|
466
696
|
throw new Error('Quota refresh is already in progress');
|
|
467
697
|
}
|
|
468
698
|
try {
|
|
@@ -472,27 +702,29 @@ export class QuotaManager {
|
|
|
472
702
|
now: this.now,
|
|
473
703
|
});
|
|
474
704
|
const now = this.now();
|
|
475
|
-
|
|
705
|
+
if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation) {
|
|
706
|
+
return {
|
|
707
|
+
quota: { ...quota, accountIdentity: accountId },
|
|
708
|
+
fetched: true,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const completedQuota = mergePollCompletionWithNewerHeaders(this.getFallback(accountId, account)?.quota, { ...quota, accountIdentity: accountId });
|
|
476
712
|
this.setFallback(accountId, {
|
|
477
713
|
quota: completedQuota,
|
|
478
714
|
refreshAfter: getQuotaNextRefreshAt(completedQuota, this.storage, now),
|
|
479
715
|
checkedAt: completedQuota.checkedAt ?? now,
|
|
480
|
-
},
|
|
716
|
+
}, account);
|
|
481
717
|
this.fallbackApiErrors.delete(accountId);
|
|
482
|
-
|
|
483
|
-
return completedQuota;
|
|
718
|
+
return { quota: completedQuota, fetched: true };
|
|
484
719
|
}
|
|
485
720
|
finally {
|
|
486
721
|
await fileLock.release();
|
|
487
722
|
}
|
|
488
723
|
}
|
|
489
724
|
catch (error) {
|
|
490
|
-
this._handleFallbackFetchError(accountId,
|
|
725
|
+
this._handleFallbackFetchError(accountId, error, generation);
|
|
491
726
|
throw error;
|
|
492
727
|
}
|
|
493
|
-
finally {
|
|
494
|
-
this.inflightFallbacks.delete(QuotaManager.fallbackInflightKey(accountId, accessToken));
|
|
495
|
-
}
|
|
496
728
|
});
|
|
497
729
|
}
|
|
498
730
|
// A 401 is an auth/token problem and a 403 is an account/org policy problem,
|
|
@@ -506,15 +738,24 @@ export class QuotaManager {
|
|
|
506
738
|
return /quota check failed: 401\b/.test(message);
|
|
507
739
|
}
|
|
508
740
|
/** Main quota failure: arms main-only backoff and persists via onApiError. */
|
|
509
|
-
_handleMainFetchError(error) {
|
|
741
|
+
_handleMainFetchError(error, generation) {
|
|
742
|
+
if (this.mainGeneration !== generation)
|
|
743
|
+
return;
|
|
744
|
+
if (this.mainQuotaIdentityKnown &&
|
|
745
|
+
this.mainQuotaIdentityStrict &&
|
|
746
|
+
this.mainAccountId === undefined) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
510
749
|
if (QuotaManager.isAuthError(error))
|
|
511
750
|
return;
|
|
512
751
|
this.mainLastApiError = buildQuotaOperationError({
|
|
513
752
|
error,
|
|
514
753
|
now: this.now(),
|
|
754
|
+
accountIdentity: this.mainAccountId,
|
|
515
755
|
previous: this.mainLastApiError,
|
|
516
756
|
});
|
|
517
|
-
this.
|
|
757
|
+
this.mainQuotaErrorGeneration += 1;
|
|
758
|
+
this.onApiError?.(this.mainLastApiError, this.mainQuotaErrorGeneration);
|
|
518
759
|
}
|
|
519
760
|
/**
|
|
520
761
|
* Fallback quota failure: arms backoff for THIS account only. Never touches
|
|
@@ -522,18 +763,17 @@ export class QuotaManager {
|
|
|
522
763
|
* quota error) — the per-account error is recorded by the caller via the
|
|
523
764
|
* account's lastQuotaRefreshError.
|
|
524
765
|
*/
|
|
525
|
-
_handleFallbackFetchError(accountId,
|
|
766
|
+
_handleFallbackFetchError(accountId, error, generation) {
|
|
767
|
+
if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation)
|
|
768
|
+
return;
|
|
526
769
|
if (QuotaManager.isAuthError(error))
|
|
527
770
|
return;
|
|
528
|
-
const
|
|
529
|
-
const previous = this.fallbackErrorTokenFps.get(accountId) === tokenFp
|
|
530
|
-
? this.fallbackApiErrors.get(accountId)
|
|
531
|
-
: undefined;
|
|
771
|
+
const previous = this.fallbackApiErrors.get(accountId);
|
|
532
772
|
this.fallbackApiErrors.set(accountId, buildQuotaOperationError({
|
|
533
773
|
error,
|
|
534
774
|
now: this.now(),
|
|
535
775
|
previous,
|
|
776
|
+
accountIdentity: accountId,
|
|
536
777
|
}));
|
|
537
|
-
this.fallbackErrorTokenFps.set(accountId, tokenFp);
|
|
538
778
|
}
|
|
539
779
|
}
|