crewx 0.9.0-rc.71 → 0.9.0-rc.72
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.
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProviderLimitCache = exports.FAIL_BACKOFF_STEPS_MS = exports.RATE_LIMITED_STALE_TTL_MS = exports.STALE_TTL_MS = exports.FRESH_TTL_MS = exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS = exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS = exports.PROVIDER_LIMIT_STALE_TTL_MS = exports.PROVIDER_LIMIT_FRESH_TTL_MS = void 0;
|
|
4
|
+
exports.buildProviderLimitCacheKey = buildProviderLimitCacheKey;
|
|
5
|
+
exports.buildProviderLimitActiveScopeKey = buildProviderLimitActiveScopeKey;
|
|
6
|
+
exports.fingerprintAccountScope = fingerprintAccountScope;
|
|
7
|
+
exports.createProviderLimitCache = createProviderLimitCache;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const cache_db_js_1 = require("../cache-db.js");
|
|
10
|
+
exports.PROVIDER_LIMIT_FRESH_TTL_MS = 5 * 60 * 1000;
|
|
11
|
+
exports.PROVIDER_LIMIT_STALE_TTL_MS = 30 * 60 * 1000;
|
|
12
|
+
exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS = [30 * 1000, 60 * 1000, 2 * 60 * 1000, 5 * 60 * 1000];
|
|
14
|
+
// These aliases make the cache policy easy to discover at call sites.
|
|
15
|
+
exports.FRESH_TTL_MS = exports.PROVIDER_LIMIT_FRESH_TTL_MS;
|
|
16
|
+
exports.STALE_TTL_MS = exports.PROVIDER_LIMIT_STALE_TTL_MS;
|
|
17
|
+
exports.RATE_LIMITED_STALE_TTL_MS = exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS;
|
|
18
|
+
exports.FAIL_BACKOFF_STEPS_MS = exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS;
|
|
19
|
+
function buildProviderLimitCacheKey(provider, accountScope, kind) {
|
|
20
|
+
return `usage:limits:v2:${provider}:${accountScope}:${kind}`;
|
|
21
|
+
}
|
|
22
|
+
function buildProviderLimitActiveScopeKey(provider) {
|
|
23
|
+
return `usage:limits:v2:${provider}:active-scope`;
|
|
24
|
+
}
|
|
25
|
+
/** Create a non-secret account discriminator from a secret account credential. */
|
|
26
|
+
function fingerprintAccountScope(value) {
|
|
27
|
+
return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
|
|
28
|
+
}
|
|
29
|
+
const VALID_STATUSES = new Set([
|
|
30
|
+
'live',
|
|
31
|
+
'cached',
|
|
32
|
+
'fallback',
|
|
33
|
+
'stale',
|
|
34
|
+
'unavailable',
|
|
35
|
+
'not_offered',
|
|
36
|
+
]);
|
|
37
|
+
const DEFAULT_SUCCESS = (data) => data !== null && hasMeaningfulData(data);
|
|
38
|
+
function hasMeaningfulData(value) {
|
|
39
|
+
if (value === null || value === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
if (Array.isArray(value))
|
|
42
|
+
return value.some((item) => hasMeaningfulData(item));
|
|
43
|
+
if (typeof value !== 'object')
|
|
44
|
+
return true;
|
|
45
|
+
const entries = Object.values(value);
|
|
46
|
+
return entries.length > 0 && entries.some((entry) => hasMeaningfulData(entry));
|
|
47
|
+
}
|
|
48
|
+
function isRecord(value) {
|
|
49
|
+
return typeof value === 'object' && value !== null;
|
|
50
|
+
}
|
|
51
|
+
function positiveFiniteOr(value, fallback) {
|
|
52
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
53
|
+
}
|
|
54
|
+
function validBackoffStepsOr(value) {
|
|
55
|
+
if (!value || value.length === 0 || value.some((step) => !Number.isFinite(step) || step <= 0)) {
|
|
56
|
+
return exports.PROVIDER_LIMIT_BACKOFF_STEPS_MS;
|
|
57
|
+
}
|
|
58
|
+
return [...value];
|
|
59
|
+
}
|
|
60
|
+
function isProviderLimitStatus(value) {
|
|
61
|
+
return VALID_STATUSES.has(value);
|
|
62
|
+
}
|
|
63
|
+
function isFailureKind(value) {
|
|
64
|
+
return value === 'not_authenticated'
|
|
65
|
+
|| value === 'unauthorized'
|
|
66
|
+
|| value === 'forbidden'
|
|
67
|
+
|| value === 'rate_limited'
|
|
68
|
+
|| value === 'timeout'
|
|
69
|
+
|| value === 'network'
|
|
70
|
+
|| value === 'upstream'
|
|
71
|
+
|| value === 'invalid_response'
|
|
72
|
+
|| value === 'client_unavailable';
|
|
73
|
+
}
|
|
74
|
+
function isProviderLimitSource(value) {
|
|
75
|
+
return value === 'codex_app_server'
|
|
76
|
+
|| value === 'codex_jsonl'
|
|
77
|
+
|| value === 'claude_oauth'
|
|
78
|
+
|| value === 'grok_oauth'
|
|
79
|
+
|| value === 'glm_oauth';
|
|
80
|
+
}
|
|
81
|
+
function isValidObservedAt(value) {
|
|
82
|
+
if (typeof value !== 'string')
|
|
83
|
+
return false;
|
|
84
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return Number.isFinite(Date.parse(value));
|
|
88
|
+
}
|
|
89
|
+
function errorFailureKind(error) {
|
|
90
|
+
if (isRecord(error) && isFailureKind(error['failureKind']))
|
|
91
|
+
return error['failureKind'];
|
|
92
|
+
if (isRecord(error) && error['name'] === 'AbortError')
|
|
93
|
+
return 'timeout';
|
|
94
|
+
if (error instanceof TypeError)
|
|
95
|
+
return 'network';
|
|
96
|
+
return 'upstream';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Persistent fresh/last-good cache for provider subscription limits.
|
|
100
|
+
*
|
|
101
|
+
* The account scope must already be a non-secret account identifier or a
|
|
102
|
+
* fingerprint. Use fingerprintAccountScope when the only available value is
|
|
103
|
+
* a credential. Failure state and in-flight work intentionally stay in memory.
|
|
104
|
+
*/
|
|
105
|
+
class ProviderLimitCache {
|
|
106
|
+
configuredProvider;
|
|
107
|
+
configuredAccountScope;
|
|
108
|
+
configuredIsSuccess;
|
|
109
|
+
now;
|
|
110
|
+
freshTtlMs;
|
|
111
|
+
staleTtlMs;
|
|
112
|
+
rateLimitedStaleTtlMs;
|
|
113
|
+
backoffStepsMs;
|
|
114
|
+
inFlight = new Map();
|
|
115
|
+
failures = new Map();
|
|
116
|
+
constructor(providerOrOptions, accountScope, isSuccessOrOptions, tuning) {
|
|
117
|
+
const baseOptions = typeof providerOrOptions === 'object' && providerOrOptions !== null
|
|
118
|
+
? providerOrOptions
|
|
119
|
+
: tuning ?? (typeof isSuccessOrOptions === 'object' ? isSuccessOrOptions : {});
|
|
120
|
+
this.configuredProvider = typeof providerOrOptions === 'string'
|
|
121
|
+
? providerOrOptions
|
|
122
|
+
: baseOptions.provider;
|
|
123
|
+
this.configuredAccountScope = typeof providerOrOptions === 'string'
|
|
124
|
+
? accountScope
|
|
125
|
+
: baseOptions.accountScope;
|
|
126
|
+
this.configuredIsSuccess = typeof providerOrOptions === 'string'
|
|
127
|
+
? typeof isSuccessOrOptions === 'function' ? isSuccessOrOptions : DEFAULT_SUCCESS
|
|
128
|
+
: baseOptions.isSuccess ?? DEFAULT_SUCCESS;
|
|
129
|
+
this.now = baseOptions.now ?? Date.now;
|
|
130
|
+
this.freshTtlMs = positiveFiniteOr(baseOptions.freshTtlMs, exports.PROVIDER_LIMIT_FRESH_TTL_MS);
|
|
131
|
+
this.staleTtlMs = positiveFiniteOr(baseOptions.staleTtlMs, exports.PROVIDER_LIMIT_STALE_TTL_MS);
|
|
132
|
+
this.rateLimitedStaleTtlMs = positiveFiniteOr(baseOptions.rateLimitedStaleTtlMs, exports.PROVIDER_LIMIT_RATE_LIMITED_STALE_TTL_MS);
|
|
133
|
+
this.backoffStepsMs = validBackoffStepsOr(baseOptions.backoffStepsMs);
|
|
134
|
+
}
|
|
135
|
+
get(...args) {
|
|
136
|
+
const request = this.resolveRequest(args);
|
|
137
|
+
const cacheKey = buildProviderLimitCacheKey(request.provider, request.accountScope, 'fresh');
|
|
138
|
+
const fresh = this.readSuccessfulEntry(cacheKey, request.isSuccess);
|
|
139
|
+
if (fresh) {
|
|
140
|
+
return Promise.resolve({ ...fresh, status: 'cached', failureKind: null });
|
|
141
|
+
}
|
|
142
|
+
const running = this.inFlight.get(cacheKey);
|
|
143
|
+
if (running)
|
|
144
|
+
return running;
|
|
145
|
+
const now = this.now();
|
|
146
|
+
const failure = this.failures.get(cacheKey);
|
|
147
|
+
if (failure && now < failure.nextAttemptAt) {
|
|
148
|
+
return Promise.resolve(this.replayFailure(request, failure.failureKind, now));
|
|
149
|
+
}
|
|
150
|
+
const operation = Promise.resolve()
|
|
151
|
+
.then(() => this.fetchAndCache(request, cacheKey))
|
|
152
|
+
.finally(() => {
|
|
153
|
+
if (this.inFlight.get(cacheKey) === operation)
|
|
154
|
+
this.inFlight.delete(cacheKey);
|
|
155
|
+
});
|
|
156
|
+
this.inFlight.set(cacheKey, operation);
|
|
157
|
+
return operation;
|
|
158
|
+
}
|
|
159
|
+
fetch(...args) {
|
|
160
|
+
return this.get(...args);
|
|
161
|
+
}
|
|
162
|
+
getOrFetch(...args) {
|
|
163
|
+
return this.get(...args);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Return the last safe account fingerprint observed for this provider.
|
|
167
|
+
* This hint is itself short-lived and contains no credential material; it
|
|
168
|
+
* lets a process restart reuse a still-fresh scoped value without probing
|
|
169
|
+
* the provider first.
|
|
170
|
+
*/
|
|
171
|
+
getActiveScope(provider = this.configuredProvider) {
|
|
172
|
+
const resolvedProvider = this.requireKeyPart(provider, 'provider');
|
|
173
|
+
const value = this.safeGet(buildProviderLimitActiveScopeKey(resolvedProvider));
|
|
174
|
+
if (typeof value === 'string' && value.length > 0 && !value.includes('\0'))
|
|
175
|
+
return value;
|
|
176
|
+
if (value !== undefined)
|
|
177
|
+
this.safeDelete(buildProviderLimitActiveScopeKey(resolvedProvider));
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
setActiveScope(provider, accountScope) {
|
|
181
|
+
const resolvedProvider = this.requireKeyPart(provider, 'provider');
|
|
182
|
+
const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
|
|
183
|
+
this.safeSet(buildProviderLimitActiveScopeKey(resolvedProvider), resolvedAccountScope, this.freshTtlMs);
|
|
184
|
+
}
|
|
185
|
+
clearActiveScope(provider = this.configuredProvider) {
|
|
186
|
+
const resolvedProvider = this.requireKeyPart(provider, 'provider');
|
|
187
|
+
this.safeDelete(buildProviderLimitActiveScopeKey(resolvedProvider));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Persist one already-observed successful result under its safe scope. This
|
|
191
|
+
* is used when the first request had no scope yet and therefore could not
|
|
192
|
+
* enter `get()` until after the single direct refresh completed.
|
|
193
|
+
*/
|
|
194
|
+
store(provider, accountScope, raw, isSuccess) {
|
|
195
|
+
const resolvedProvider = this.requireKeyPart(provider, 'provider');
|
|
196
|
+
const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
|
|
197
|
+
const predicate = isSuccess ?? this.configuredIsSuccess;
|
|
198
|
+
const result = this.normalizeResult(raw);
|
|
199
|
+
if (!this.isSuccessfulResult(result, predicate))
|
|
200
|
+
return undefined;
|
|
201
|
+
this.storeSuccessful(resolvedProvider, resolvedAccountScope, result);
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Retain the in-memory retry/backoff state for a direct cold refresh that
|
|
206
|
+
* completed before a scoped `get()` could be entered.
|
|
207
|
+
*/
|
|
208
|
+
recordFailure(provider, accountScope, raw) {
|
|
209
|
+
const resolvedProvider = this.requireKeyPart(provider, 'provider');
|
|
210
|
+
const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
|
|
211
|
+
const result = this.normalizeResult(raw);
|
|
212
|
+
if (result.status === 'not_offered') {
|
|
213
|
+
this.deletePersistentEntries(resolvedProvider, resolvedAccountScope);
|
|
214
|
+
this.clearActiveScope(resolvedProvider);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.recordFailureState(resolvedProvider, resolvedAccountScope, result);
|
|
218
|
+
}
|
|
219
|
+
clear(provider = this.configuredProvider, accountScope = this.configuredAccountScope) {
|
|
220
|
+
const resolvedProvider = this.requireKeyPart(provider, 'provider');
|
|
221
|
+
const resolvedAccountScope = this.requireKeyPart(accountScope, 'accountScope');
|
|
222
|
+
this.deletePersistentEntries(resolvedProvider, resolvedAccountScope);
|
|
223
|
+
this.failures.delete(buildProviderLimitCacheKey(resolvedProvider, resolvedAccountScope, 'fresh'));
|
|
224
|
+
}
|
|
225
|
+
reset(provider = this.configuredProvider, accountScope = this.configuredAccountScope) {
|
|
226
|
+
this.clear(provider, accountScope);
|
|
227
|
+
}
|
|
228
|
+
resolveRequest(args) {
|
|
229
|
+
const first = args[0];
|
|
230
|
+
if (typeof first === 'function') {
|
|
231
|
+
const provider = this.requireKeyPart(this.configuredProvider, 'provider');
|
|
232
|
+
const accountScope = this.requireKeyPart(this.configuredAccountScope, 'accountScope');
|
|
233
|
+
const predicate = typeof args[1] === 'function' ? args[1] : this.configuredIsSuccess;
|
|
234
|
+
return { provider, accountScope, fetcher: first, isSuccess: predicate };
|
|
235
|
+
}
|
|
236
|
+
if (typeof first === 'string') {
|
|
237
|
+
const provider = this.requireKeyPart(first, 'provider');
|
|
238
|
+
const account = args[1];
|
|
239
|
+
const fetcher = args[2];
|
|
240
|
+
if (typeof account !== 'string' || typeof fetcher !== 'function') {
|
|
241
|
+
throw new TypeError('ProviderLimitCache.get requires provider, accountScope, and fetcher');
|
|
242
|
+
}
|
|
243
|
+
const predicate = typeof args[3] === 'function' ? args[3] : this.configuredIsSuccess;
|
|
244
|
+
return { provider, accountScope: this.requireKeyPart(account, 'accountScope'), fetcher, isSuccess: predicate };
|
|
245
|
+
}
|
|
246
|
+
const fetcher = first.fetcher ?? first.fetch;
|
|
247
|
+
if (typeof fetcher !== 'function')
|
|
248
|
+
throw new TypeError('ProviderLimitCache request requires fetcher');
|
|
249
|
+
return {
|
|
250
|
+
provider: this.requireKeyPart(first.provider, 'provider'),
|
|
251
|
+
accountScope: this.requireKeyPart(first.accountScope, 'accountScope'),
|
|
252
|
+
fetcher,
|
|
253
|
+
isSuccess: first.isSuccess ?? this.configuredIsSuccess,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async fetchAndCache(request, cacheKey) {
|
|
257
|
+
let result;
|
|
258
|
+
const scopeObservation = { present: false, accountScope: null };
|
|
259
|
+
try {
|
|
260
|
+
const raw = await request.fetcher();
|
|
261
|
+
const rawRecord = isRecord(raw) ? raw : undefined;
|
|
262
|
+
scopeObservation.present = rawRecord !== undefined
|
|
263
|
+
&& Object.prototype.hasOwnProperty.call(rawRecord, 'cacheAccountScope');
|
|
264
|
+
if (scopeObservation.present && rawRecord) {
|
|
265
|
+
const observed = rawRecord['cacheAccountScope'];
|
|
266
|
+
scopeObservation.accountScope = typeof observed === 'string'
|
|
267
|
+
&& observed.length > 0
|
|
268
|
+
&& !observed.includes('\0')
|
|
269
|
+
? observed
|
|
270
|
+
: null;
|
|
271
|
+
}
|
|
272
|
+
result = this.normalizeResult(raw);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
result = this.unavailableResult(errorFailureKind(error));
|
|
276
|
+
}
|
|
277
|
+
if (scopeObservation.present && scopeObservation.accountScope !== request.accountScope) {
|
|
278
|
+
// A direct refresh is authoritative about the account that was actually
|
|
279
|
+
// observed. Never replay or store account A data when that observation
|
|
280
|
+
// says B or says that no safe account scope exists.
|
|
281
|
+
this.deletePersistentEntries(request.provider, request.accountScope);
|
|
282
|
+
this.failures.delete(cacheKey);
|
|
283
|
+
if (scopeObservation.accountScope === null) {
|
|
284
|
+
this.clearActiveScope(request.provider);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
this.setActiveScope(request.provider, scopeObservation.accountScope);
|
|
288
|
+
if (this.isSuccessfulResult(result, request.isSuccess)) {
|
|
289
|
+
this.storeSuccessful(request.provider, scopeObservation.accountScope, result);
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
if (result.status === 'not_offered') {
|
|
294
|
+
this.deletePersistentEntries(request.provider, request.accountScope);
|
|
295
|
+
this.failures.delete(cacheKey);
|
|
296
|
+
this.clearActiveScope(request.provider);
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
if (this.isSuccessfulResult(result, request.isSuccess)) {
|
|
300
|
+
this.failures.delete(cacheKey);
|
|
301
|
+
this.storeSuccessful(request.provider, request.accountScope, result);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
const now = this.now();
|
|
305
|
+
const failureKind = this.recordFailureState(request.provider, request.accountScope, result);
|
|
306
|
+
return this.replayFailure(request, failureKind, now);
|
|
307
|
+
}
|
|
308
|
+
recordFailureState(provider, accountScope, result) {
|
|
309
|
+
const cacheKey = buildProviderLimitCacheKey(provider, accountScope, 'fresh');
|
|
310
|
+
const failureKind = this.hasInvalidObservationMetadata(result)
|
|
311
|
+
? 'invalid_response'
|
|
312
|
+
: result.failureKind ?? 'invalid_response';
|
|
313
|
+
const now = this.now();
|
|
314
|
+
const previous = this.failures.get(cacheKey);
|
|
315
|
+
const stepIndex = Math.min(previous?.count ?? 0, this.backoffStepsMs.length - 1);
|
|
316
|
+
this.failures.set(cacheKey, {
|
|
317
|
+
count: (previous?.count ?? 0) + 1,
|
|
318
|
+
nextAttemptAt: now + this.backoffStepsMs[stepIndex],
|
|
319
|
+
failureKind,
|
|
320
|
+
});
|
|
321
|
+
return failureKind;
|
|
322
|
+
}
|
|
323
|
+
readSuccessfulEntry(key, isSuccess) {
|
|
324
|
+
const stored = this.safeGet(key);
|
|
325
|
+
if (stored === undefined)
|
|
326
|
+
return undefined;
|
|
327
|
+
const normalized = this.normalizeResult(stored);
|
|
328
|
+
if (this.isSuccessfulResult(normalized, isSuccess))
|
|
329
|
+
return normalized;
|
|
330
|
+
this.safeDelete(key);
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
readLastGood(request) {
|
|
334
|
+
const key = buildProviderLimitCacheKey(request.provider, request.accountScope, 'last-good');
|
|
335
|
+
return this.readSuccessfulEntry(key, request.isSuccess);
|
|
336
|
+
}
|
|
337
|
+
replayFailure(request, failureKind, now) {
|
|
338
|
+
const lastGood = this.readLastGood(request);
|
|
339
|
+
if (lastGood && this.isWithinStaleWindow(lastGood, failureKind, now)) {
|
|
340
|
+
return {
|
|
341
|
+
...lastGood,
|
|
342
|
+
status: 'stale',
|
|
343
|
+
failureKind,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return this.unavailableResult(failureKind);
|
|
347
|
+
}
|
|
348
|
+
isWithinStaleWindow(lastGood, failureKind, now) {
|
|
349
|
+
if (!lastGood.observedAt)
|
|
350
|
+
return false;
|
|
351
|
+
const observedAt = Date.parse(lastGood.observedAt);
|
|
352
|
+
if (!Number.isFinite(observedAt))
|
|
353
|
+
return false;
|
|
354
|
+
const ttl = failureKind === 'rate_limited' ? this.rateLimitedStaleTtlMs : this.staleTtlMs;
|
|
355
|
+
return now - observedAt < ttl;
|
|
356
|
+
}
|
|
357
|
+
isSuccessfulResult(result, isSuccess) {
|
|
358
|
+
if (result.status !== 'live' && result.status !== 'fallback')
|
|
359
|
+
return false;
|
|
360
|
+
if (!isValidObservedAt(result.observedAt) || !isProviderLimitSource(result.source))
|
|
361
|
+
return false;
|
|
362
|
+
if (result.data === null || result.data === undefined || !hasMeaningfulData(result.data))
|
|
363
|
+
return false;
|
|
364
|
+
try {
|
|
365
|
+
return isSuccess(result.data);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
hasInvalidObservationMetadata(result) {
|
|
372
|
+
return (result.status === 'live' || result.status === 'fallback')
|
|
373
|
+
&& result.data !== null
|
|
374
|
+
&& result.data !== undefined
|
|
375
|
+
&& hasMeaningfulData(result.data)
|
|
376
|
+
&& (!isValidObservedAt(result.observedAt) || !isProviderLimitSource(result.source));
|
|
377
|
+
}
|
|
378
|
+
normalizeResult(raw) {
|
|
379
|
+
if (!isRecord(raw) || typeof raw['status'] !== 'string' || !isProviderLimitStatus(raw['status']) || !('data' in raw)) {
|
|
380
|
+
return this.unavailableResult('invalid_response');
|
|
381
|
+
}
|
|
382
|
+
const failureKind = raw['failureKind'];
|
|
383
|
+
const source = raw['source'];
|
|
384
|
+
const observedAt = raw['observedAt'];
|
|
385
|
+
return {
|
|
386
|
+
status: raw['status'],
|
|
387
|
+
data: raw['data'],
|
|
388
|
+
failureKind: failureKind === null || failureKind === undefined
|
|
389
|
+
? null
|
|
390
|
+
: isFailureKind(failureKind) ? failureKind : 'invalid_response',
|
|
391
|
+
observedAt: observedAt === null || observedAt === undefined
|
|
392
|
+
? null
|
|
393
|
+
: typeof observedAt === 'string' ? observedAt : null,
|
|
394
|
+
source: source === null || source === undefined
|
|
395
|
+
? null
|
|
396
|
+
: isProviderLimitSource(source) ? source : null,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
unavailableResult(failureKind) {
|
|
400
|
+
return {
|
|
401
|
+
status: 'unavailable',
|
|
402
|
+
data: null,
|
|
403
|
+
failureKind,
|
|
404
|
+
observedAt: null,
|
|
405
|
+
source: null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
storeSuccessful(provider, accountScope, result) {
|
|
409
|
+
const cacheKey = buildProviderLimitCacheKey(provider, accountScope, 'fresh');
|
|
410
|
+
this.safeSet(cacheKey, result, this.freshTtlMs);
|
|
411
|
+
this.safeSet(buildProviderLimitCacheKey(provider, accountScope, 'last-good'), result, Math.max(this.staleTtlMs, this.rateLimitedStaleTtlMs));
|
|
412
|
+
this.setActiveScope(provider, accountScope);
|
|
413
|
+
}
|
|
414
|
+
requireKeyPart(value, name) {
|
|
415
|
+
if (!value || value.includes('\0'))
|
|
416
|
+
throw new TypeError(`ProviderLimitCache requires a non-empty ${name}`);
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
deletePersistentEntries(provider, accountScope) {
|
|
420
|
+
this.safeDelete(buildProviderLimitCacheKey(provider, accountScope, 'fresh'));
|
|
421
|
+
this.safeDelete(buildProviderLimitCacheKey(provider, accountScope, 'last-good'));
|
|
422
|
+
}
|
|
423
|
+
safeGet(key) {
|
|
424
|
+
try {
|
|
425
|
+
return (0, cache_db_js_1.kvGet)(key);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
safeSet(key, value, ttlMs) {
|
|
432
|
+
try {
|
|
433
|
+
(0, cache_db_js_1.kvSet)(key, value, ttlMs);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// Cache persistence is best effort; the live provider result remains authoritative.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
safeDelete(key) {
|
|
440
|
+
try {
|
|
441
|
+
(0, cache_db_js_1.kvDel)(key);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Cache cleanup is best effort.
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
exports.ProviderLimitCache = ProviderLimitCache;
|
|
449
|
+
function createProviderLimitCache(options = {}) {
|
|
450
|
+
return new ProviderLimitCache(options);
|
|
451
|
+
}
|