@goodandready/dsh-key-rotation 0.8.68 → 0.8.70

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/lib/pool.js CHANGED
@@ -1,28 +1,15 @@
1
- export const MAX_SESSION_STICKY_ENTRIES = 500;
2
-
3
- export function setSessionSticky(pool, sessionId, candidate) {
4
- if (!sessionId || !candidate || !pool?.state) return;
5
- if (!(pool.state.sessionSticky instanceof Map)) {
6
- pool.state.sessionSticky = new Map();
7
- }
8
- if (pool.state.sessionSticky.has(sessionId)) {
9
- pool.state.sessionSticky.delete(sessionId);
10
- } else if (pool.state.sessionSticky.size >= MAX_SESSION_STICKY_ENTRIES) {
11
- const oldest = pool.state.sessionSticky.keys().next().value;
12
- if (oldest !== undefined) pool.state.sessionSticky.delete(oldest);
13
- }
14
- pool.state.sessionSticky.set(sessionId, candidate);
15
- }
16
-
17
- export function getSessionSticky(pool, sessionId) {
18
- if (!sessionId || !pool?.state?.sessionSticky) return null;
19
- return pool.state.sessionSticky.get(sessionId) ?? null;
20
- }
21
-
22
- export function clearSessionSticky(pool, sessionId) {
23
- if (!sessionId || !pool?.state?.sessionSticky) return;
24
- pool.state.sessionSticky.delete(sessionId);
25
- }
1
+ // lib/pool.js — pure pool arithmetic and selection logic for dsh-key-rotation.
2
+ import {
3
+ MAX_SESSION_STICKY_ENTRIES, setSessionSticky, getSessionSticky, clearSessionSticky,
4
+ selectPool, sortAttemptList, expiringSoon, shouldNotifyDaily,
5
+ costForDay, costForWeek, budgetVerdict,
6
+ } from './pool-selection.js';
7
+
8
+ export {
9
+ MAX_SESSION_STICKY_ENTRIES, setSessionSticky, getSessionSticky, clearSessionSticky,
10
+ selectPool, sortAttemptList, expiringSoon, shouldNotifyDaily,
11
+ costForDay, costForWeek, budgetVerdict,
12
+ };
26
13
 
27
14
  import { resetModelQuotaIfNeeded } from './model-quota.js';
28
15
  import { decryptSecret } from './crypto-storage.js';
@@ -239,6 +226,7 @@ export function sweepExpired(poolState, now = Date.now(), activeRefs = null, quo
239
226
  let cleared = 0;
240
227
  const activeSet = activeRefs ? new Set(activeRefs) : null;
241
228
  for (const st of poolState.values()) {
229
+ if (!st || !(st.failedUntil instanceof Map)) continue;
242
230
  // #338: decay penalty failCounts for stable keys in maintenance sweep
243
231
  decayPenalties({ state: st }, now);
244
232
  for (const [ref, until] of [...st.failedUntil.entries()]) {
@@ -288,19 +276,6 @@ export function parseRetryAfter(value) {
288
276
  }
289
277
 
290
278
  /** Pick a key pool for a (provider, model) pair. */
291
- export function selectPool(modelPoolByProvider, providerToPool, provider, model) {
292
- const byModel = modelPoolByProvider && modelPoolByProvider.get(provider);
293
- const base = providerToPool && providerToPool.get(provider);
294
- if (!byModel || !model) return base ?? null;
295
- if (byModel.has(model)) return byModel.get(model);
296
- let best = null;
297
- for (const key of byModel.keys()) {
298
- if (model.startsWith(key) && key.length > (best ? best.length : 0)) best = key;
299
- }
300
- return (best ? byModel.get(best) : base) ?? null;
301
- }
302
-
303
- /** Parse an expiry value (timestamp ms or ISO date string) to epoch ms. */
304
279
  export function parseExpiry(v) {
305
280
  if (typeof v === 'number' && v > 0) return v;
306
281
  if (typeof v === 'string' && v.length > 0) { const ts = Date.parse(v); return Number.isNaN(ts) ? undefined : ts; }
@@ -434,61 +409,6 @@ export function isRateLimited(rate, threshold = 0.1) {
434
409
  * - 'least-loaded': keys with lowest active concurrency count first
435
410
  * - 'lowest-latency': keys with lowest p95 (or avg) latency first; unsampled keys neutral
436
411
  */
437
- export function sortAttemptList(refs, strategy = 'round-robin', deps = {}) {
438
- if (!Array.isArray(refs) || refs.length <= 1) return (refs ?? []).slice();
439
- const list = refs.slice();
440
- if (strategy === 'sticky') {
441
- const last = deps.sessionSticky ||
442
- (deps.sessionId && (deps.pool?.state?.sessionSticky?.get?.(deps.sessionId) || deps.sessionStickyMap?.get?.(deps.sessionId))) ||
443
- deps.lastUsed ||
444
- (deps.pool && deps.pool.state && deps.pool.state.lastUsed);
445
- if (last && list.includes(last)) {
446
- const idx = list.indexOf(last);
447
- if (idx > 0) {
448
- list.splice(idx, 1);
449
- list.unshift(last);
450
- }
451
- }
452
- return list;
453
- }
454
- if (strategy === 'tier-priority') {
455
- const tiers = deps.tiers || (deps.pool && deps.pool.tiers) || {};
456
- return list.sort((a, b) => {
457
- const ta = (tiers[a] && Number(tiers[a]) >= 1) ? Number(tiers[a]) : 1;
458
- const tb = (tiers[b] && Number(tiers[b]) >= 1) ? Number(tiers[b]) : 1;
459
- return ta - tb;
460
- });
461
- }
462
- if (strategy === 'least-loaded' && deps.concurrencyTracker && typeof deps.concurrencyTracker.getActive === 'function') {
463
- const weights = deps.weights || (deps.pool && deps.pool.weightsMap) || {};
464
- return list.sort((a, b) => {
465
- const ca = deps.concurrencyTracker.getActive(a) ?? 0;
466
- const cb = deps.concurrencyTracker.getActive(b) ?? 0;
467
- const wa = (weights[a] && Number(weights[a]) > 0) ? Number(weights[a]) : 1;
468
- const wb = (weights[b] && Number(weights[b]) > 0) ? Number(weights[b]) : 1;
469
- const scoreA = ca / wa;
470
- const scoreB = cb / wb;
471
- if (scoreA !== scoreB) return scoreA - scoreB;
472
- return ca - cb;
473
- });
474
- }
475
- if (strategy === 'lowest-latency' && deps.latencyHistogram && typeof deps.latencyHistogram.snapshot === 'function') {
476
- return list.sort((a, b) => {
477
- const sa = deps.latencyHistogram.snapshot(a);
478
- const sb = deps.latencyHistogram.snapshot(b);
479
- const la = (sa && Number.isFinite(sa.p95)) ? sa.p95 : ((sa && Number.isFinite(sa.avg)) ? sa.avg : 500);
480
- const lb = (sb && Number.isFinite(sb.p95)) ? sb.p95 : ((sb && Number.isFinite(sb.avg)) ? sb.avg : 500);
481
- return la - lb;
482
- });
483
- }
484
- return list;
485
- }
486
-
487
- /**
488
- * Unified check whether an error or payload represents a switchable failure.
489
- * Prioritizes explicit HTTP status codes and gRPC codes before text matching.
490
- */
491
- /** Raw socket-level codes the taxonomy maps to TRANSPORT (see classifyFailure). */
492
412
  const SOCKET_TRANSPORT_CODES = new Set([
493
413
  'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND',
494
414
  'ECONNABORTED', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_SOCKET', 'UND_ERR_HEADERS_TIMEOUT',
@@ -565,58 +485,6 @@ export function formatExhaustionMessage(provider, pool, now = Date.now()) {
565
485
  * Keys of pool whose expiresAt falls within the next warnDays.
566
486
  * Returns [{ ref, expiresInDays, expiresAt }], soonest first.
567
487
  */
568
- export function expiringSoon(pool, warnDays = 7, now = Date.now()) {
569
- if (!pool || !pool.expiresAt) return [];
570
- const DAY_MS = 86400000;
571
- const horizon = now + Math.max(1, warnDays) * DAY_MS;
572
- return Object.entries(pool.expiresAt)
573
- .filter(([, at]) => at > now && at <= horizon)
574
- .map(([ref, at]) => ({ ref, expiresAt: at, expiresInDays: Math.max(0, Math.floor((at - now) / DAY_MS)) }))
575
- .sort((a, b) => a.expiresAt - b.expiresAt);
576
- }
577
-
578
- /** Dedupe: true when a day-level notification for key is due. */
579
- export function shouldNotifyDaily(lastNotified, key, now = Date.now()) {
580
- const DAY_MS = 86400000;
581
- if (!lastNotified.has(key)) {
582
- lastNotified.set(key, now);
583
- return true;
584
- }
585
- const last = lastNotified.get(key);
586
- if (now - last < DAY_MS) return false;
587
- lastNotified.set(key, now);
588
- return true;
589
- }
590
-
591
- /** Total spend of a pool on ISO day day (defaults to today) across costDays Map<ref, Map<day, cost>>. */
592
- export function costForDay(costDays, day) {
593
- const d = day ?? new Date().toISOString().slice(0, 10);
594
- let total = 0;
595
- for (const perRef of (costDays?.values() ?? [])) {
596
- total += perRef.get(d) ?? 0;
597
- }
598
- return total;
599
- }
600
-
601
- /** Total spend over the last 7 ISO days ending today. */
602
- export function costForWeek(costDays, now = Date.now()) {
603
- let total = 0;
604
- for (let i = 0; i < 7; i++) {
605
- const d = new Date(now - i * 86400000).toISOString().slice(0, 10);
606
- total += costForDay(costDays, d);
607
- }
608
- return total;
609
- }
610
-
611
- /** Budget verdict for a pool: { spend, budget, ratio, warn, exceeded }. */
612
- export function budgetVerdict(spend, budget) {
613
- if (!budget || budget <= 0) return { spend, budget: 0, ratio: 0, warn: false, exceeded: false };
614
- const ratio = spend / budget;
615
- return { spend, budget, ratio, warn: ratio >= 0.8, exceeded: ratio >= 1 };
616
- }
617
-
618
-
619
- /** Safely reset a provider's circuit breaker to closed state. */
620
488
  export function resetCircuitForProvider(circuitBreaker, provider) {
621
489
  if (!circuitBreaker || !provider) return false;
622
490
  const ok = bestEffort('resetCircuitForProvider', () => {