@goodandready/dsh-key-rotation 0.8.73 → 0.8.75
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 +23 -0
- package/lib/atomic-io.js +47 -4
- package/lib/client.js +178 -0
- package/lib/clone-lifecycle.js +5 -5
- package/lib/concurrency.js +25 -5
- package/lib/index.js +19 -4
- package/lib/key-drain.js +76 -41
- package/lib/lifecycle.js +4 -2
- package/lib/ops-config-io.js +40 -15
- package/lib/ops-keys.js +6 -5
- package/lib/ops-quick-action.js +6 -5
- package/lib/ops-telemetry.js +51 -16
- package/lib/pool-builder.js +16 -1
- package/lib/pool.js +10 -4
- package/lib/request-budget.js +8 -1
- package/lib/resolver.js +1 -1
- package/lib/rotate.js +628 -595
- package/lib/safe-fetch.js +30 -28
- package/lib/selection-trace.js +32 -11
- package/lib/simulator.js +8 -2
- package/lib/traffic-smoother.js +31 -20
- package/package.json +9 -12
package/lib/ops-config-io.js
CHANGED
|
@@ -224,7 +224,7 @@ export function registerConfigIoRoutes(ctx, deps) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
const settings = ctx.get('settings');
|
|
227
|
+
const settings = typeof ctx?.get === 'function' ? ctx.get('settings') : (ctx?.settings || null);
|
|
228
228
|
let currentConfig = {};
|
|
229
229
|
try {
|
|
230
230
|
if (settings && typeof settings.get === 'function') {
|
|
@@ -322,28 +322,54 @@ export function registerConfigIoRoutes(ctx, deps) {
|
|
|
322
322
|
const mode = body.mode === 'replace' ? 'replace' : 'merge';
|
|
323
323
|
const dryRun = Boolean(body.dryRun);
|
|
324
324
|
|
|
325
|
-
const settings = ctx.get('settings');
|
|
325
|
+
const settings = typeof ctx?.get === 'function' ? ctx.get('settings') : (ctx?.settings || null);
|
|
326
|
+
if (!settings || typeof settings.replace !== 'function') {
|
|
327
|
+
// Fail closed when settings service is missing or lacks replace (#533)
|
|
328
|
+
json(res, 503, { error: { code: 'settings-unavailable', message: 'Settings service is not available or does not support writing configuration' } });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
326
332
|
let currentConfig = {};
|
|
333
|
+
let currentRevision = null;
|
|
327
334
|
try {
|
|
328
|
-
if (
|
|
329
|
-
currentConfig = settings.get(NS) || {};
|
|
330
|
-
} else if (settings && typeof settings.describe === 'function') {
|
|
335
|
+
if (typeof settings.describe === 'function') {
|
|
331
336
|
const desc = settings.describe({ redactSecrets: false })?.find((c) => c.ns === NS);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
337
|
+
if (desc) {
|
|
338
|
+
currentConfig = desc.value || {};
|
|
339
|
+
currentRevision = desc.revision ?? null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!currentConfig || Object.keys(currentConfig).length === 0) {
|
|
343
|
+
if (typeof settings.get === 'function') {
|
|
344
|
+
currentConfig = settings.get(NS) || {};
|
|
345
|
+
}
|
|
335
346
|
}
|
|
336
347
|
} catch (_) {
|
|
337
348
|
currentConfig = deps.buildRuntime().cfg0 || {};
|
|
338
349
|
}
|
|
339
350
|
const rawCurrent = plainConfig(currentConfig);
|
|
340
351
|
|
|
352
|
+
// Concurrency control: if client provided expectedRevision, reject if stale (#553)
|
|
353
|
+
if (typeof body.expectedRevision === 'number' && currentRevision !== null) {
|
|
354
|
+
if (body.expectedRevision !== currentRevision) {
|
|
355
|
+
json(res, 409, {
|
|
356
|
+
error: {
|
|
357
|
+
code: 'conflict',
|
|
358
|
+
message: `Configuration has been modified concurrently (expected revision ${body.expectedRevision}, current is ${currentRevision})`,
|
|
359
|
+
currentRevision,
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
341
366
|
const preview = diffConfigs(rawCurrent, incomingConfig, mode);
|
|
342
367
|
|
|
343
368
|
if (dryRun) {
|
|
344
369
|
json(res, 200, {
|
|
345
370
|
ok: true,
|
|
346
371
|
dryRun: true,
|
|
372
|
+
revision: currentRevision,
|
|
347
373
|
preview,
|
|
348
374
|
});
|
|
349
375
|
return;
|
|
@@ -379,19 +405,18 @@ export function registerConfigIoRoutes(ctx, deps) {
|
|
|
379
405
|
mergedConfig.providers = curProviders;
|
|
380
406
|
}
|
|
381
407
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
408
|
+
try {
|
|
409
|
+
await settings.replace(NS, mergedConfig, currentRevision ?? undefined);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
json(res, 500, { error: { code: 'settings-write-failed', message: err.message } });
|
|
412
|
+
return;
|
|
389
413
|
}
|
|
390
414
|
|
|
391
415
|
json(res, 200, {
|
|
392
416
|
ok: true,
|
|
393
417
|
applied: true,
|
|
394
418
|
mode,
|
|
419
|
+
revision: typeof currentRevision === 'number' ? currentRevision + 1 : undefined,
|
|
395
420
|
preview,
|
|
396
421
|
});
|
|
397
422
|
};
|
package/lib/ops-keys.js
CHANGED
|
@@ -165,17 +165,18 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
const concurrency = deps.concurrencyTracker;
|
|
168
|
+
const keyDrainManager = deps.keyDrainManager;
|
|
168
169
|
|
|
169
170
|
if (req.method === 'GET') {
|
|
170
171
|
const url = new URL(req.url ?? DRAIN_PATH, 'http://localhost');
|
|
171
172
|
const ref = url.searchParams.get('ref');
|
|
172
173
|
if (ref) {
|
|
173
|
-
const status = getDrainingStatus(ref, concurrency);
|
|
174
|
+
const status = getDrainingStatus(ref, concurrency, keyDrainManager);
|
|
174
175
|
if (!status) { json(res, 404, { error: { code: 'not-found', message: `Key ${ref} is not currently draining` } }); return; }
|
|
175
176
|
json(res, 200, { ok: true, draining: status });
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
178
|
-
json(res, 200, { ok: true, draining: getAllDrainingKeys(concurrency) });
|
|
179
|
+
json(res, 200, { ok: true, draining: getAllDrainingKeys(concurrency, keyDrainManager) });
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -187,8 +188,8 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
187
188
|
}
|
|
188
189
|
const ref = body?.ref;
|
|
189
190
|
if (!ref) { json(res, 400, { error: { code: 'bad-request', message: 'Missing ref' } }); return; }
|
|
190
|
-
markKeyDraining(ref, body?.provider || '');
|
|
191
|
-
json(res, 200, { ok: true, status: getDrainingStatus(ref, concurrency) });
|
|
191
|
+
markKeyDraining(ref, body?.provider || '', keyDrainManager);
|
|
192
|
+
json(res, 200, { ok: true, status: getDrainingStatus(ref, concurrency, keyDrainManager) });
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -196,7 +197,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
196
197
|
const url = new URL(req.url ?? DRAIN_PATH, 'http://localhost');
|
|
197
198
|
const ref = url.searchParams.get('ref');
|
|
198
199
|
if (!ref) { json(res, 400, { error: { code: 'bad-request', message: 'Missing ref query parameter' } }); return; }
|
|
199
|
-
const cancelled = cancelKeyDrain(ref);
|
|
200
|
+
const cancelled = cancelKeyDrain(ref, keyDrainManager);
|
|
200
201
|
json(res, 200, { ok: true, cancelled });
|
|
201
202
|
return;
|
|
202
203
|
}
|
package/lib/ops-quick-action.js
CHANGED
|
@@ -66,10 +66,11 @@ export function registerQuickActionRoutes(ctx, deps) {
|
|
|
66
66
|
json(res, 400, { error: { code: 'no-keys', message: 'Pool has no keys to rotate' } });
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
targetPool.state.
|
|
72
|
-
|
|
69
|
+
const currentPointer = targetPool.state.pointer ?? targetPool.state.cursor ?? 0;
|
|
70
|
+
const nextPointer = (currentPointer + 1) % total;
|
|
71
|
+
targetPool.state.pointer = nextPointer;
|
|
72
|
+
targetPool.state.cursor = nextPointer;
|
|
73
|
+
const nextRef = targetPool.refs[nextPointer];
|
|
73
74
|
targetPool.state.lastUsed = nextRef;
|
|
74
75
|
|
|
75
76
|
if (typeof pushEvent === 'function') {
|
|
@@ -80,7 +81,7 @@ export function registerQuickActionRoutes(ctx, deps) {
|
|
|
80
81
|
ok: true,
|
|
81
82
|
action: 'skip',
|
|
82
83
|
provider,
|
|
83
|
-
cursor:
|
|
84
|
+
cursor: nextPointer,
|
|
84
85
|
activeRef: nextRef,
|
|
85
86
|
activeTail: keyTail(nextRef),
|
|
86
87
|
});
|
package/lib/ops-telemetry.js
CHANGED
|
@@ -30,20 +30,40 @@ import { getPromptCacheSummary } from './prompt-cache.js';
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
const DIAG_BUFFER_LIMIT = 200;
|
|
33
|
-
const diagBuffer = [];
|
|
34
33
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
export class DiagBuffer {
|
|
35
|
+
constructor(limit = DIAG_BUFFER_LIMIT) {
|
|
36
|
+
this.limit = limit;
|
|
37
|
+
this.buffer = [];
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
|
|
40
|
+
push(entry) {
|
|
41
|
+
if (this.buffer.length >= this.limit) {
|
|
42
|
+
this.buffer.shift();
|
|
43
|
+
}
|
|
44
|
+
this.buffer.push({
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
...entry,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get() {
|
|
51
|
+
return this.buffer.slice();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clear() {
|
|
55
|
+
this.buffer.length = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultDiagBuffer = new DiagBuffer();
|
|
60
|
+
|
|
61
|
+
export function pushDiag(entry, buffer = null) {
|
|
62
|
+
(buffer || defaultDiagBuffer).push(entry);
|
|
43
63
|
}
|
|
44
64
|
|
|
45
|
-
export function getDiagBuffer() {
|
|
46
|
-
return
|
|
65
|
+
export function getDiagBuffer(buffer = null) {
|
|
66
|
+
return (buffer || defaultDiagBuffer).get();
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
export function registerTelemetryRoutes(ctx, deps) {
|
|
@@ -154,14 +174,15 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
154
174
|
json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: trace is local-only' } });
|
|
155
175
|
return;
|
|
156
176
|
}
|
|
177
|
+
const traceStore = deps.selectionTraceStore || null;
|
|
157
178
|
if (req.method === 'DELETE') {
|
|
158
|
-
clearSelectionTraces();
|
|
179
|
+
clearSelectionTraces(traceStore);
|
|
159
180
|
json(res, 200, { ok: true, message: 'trace history cleared' });
|
|
160
181
|
return;
|
|
161
182
|
}
|
|
162
183
|
const url = new URL(req.url ?? TRACE_PATH, 'http://localhost');
|
|
163
184
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit')) || 50));
|
|
164
|
-
json(res, 200, { ok: true, traces: getSelectionTraces(limit) });
|
|
185
|
+
json(res, 200, { ok: true, traces: getSelectionTraces(limit, traceStore) });
|
|
165
186
|
},
|
|
166
187
|
}), 'dsh-key-rotation: trace route');
|
|
167
188
|
|
|
@@ -185,11 +206,25 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
const runtime = buildRuntime();
|
|
188
|
-
const provider = body?.provider
|
|
189
|
-
|
|
209
|
+
const provider = body?.provider?.trim();
|
|
210
|
+
if (!provider) {
|
|
211
|
+
json(res, 400, { error: { code: 'bad-request', message: 'provider is required for simulation' } });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let pool = null;
|
|
216
|
+
if (runtime.providerToPool instanceof Map) {
|
|
217
|
+
pool = runtime.providerToPool.get(provider);
|
|
218
|
+
} else if (runtime.providerToPool && typeof runtime.providerToPool === 'object') {
|
|
219
|
+
pool = runtime.providerToPool[provider];
|
|
220
|
+
}
|
|
221
|
+
if (!pool) {
|
|
222
|
+
const pools = runtime.pools ?? [];
|
|
223
|
+
pool = pools.find(p => p.provider === provider || p.base === provider);
|
|
224
|
+
}
|
|
190
225
|
|
|
191
226
|
if (!pool) {
|
|
192
|
-
json(res, 404, { error: { code: 'not-found', message: `Provider '${provider}' not found` } });
|
|
227
|
+
json(res, 404, { error: { code: 'not-found', message: `Provider '${provider}' not found in configured pools` } });
|
|
193
228
|
return;
|
|
194
229
|
}
|
|
195
230
|
|
|
@@ -234,7 +269,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
234
269
|
json(res, 200, { ok: true, count: diagBuffer.length });
|
|
235
270
|
return;
|
|
236
271
|
}
|
|
237
|
-
json(res, 200, { diag: getDiagBuffer() });
|
|
272
|
+
json(res, 200, { diag: deps.diagBuffer ? deps.diagBuffer.get() : getDiagBuffer() });
|
|
238
273
|
},
|
|
239
274
|
}), 'dsh-key-rotation: diag route');
|
|
240
275
|
|
package/lib/pool-builder.js
CHANGED
|
@@ -39,6 +39,8 @@ export function buildPoolItem({
|
|
|
39
39
|
useKeyRef = null,
|
|
40
40
|
trafficSmoothingBurst = null,
|
|
41
41
|
maxQueueWaitMs = null,
|
|
42
|
+
maxAttempts = null,
|
|
43
|
+
maxDurationMs = null,
|
|
42
44
|
}) {
|
|
43
45
|
const seen = new Set();
|
|
44
46
|
const indexed = (Array.isArray(keys) ? keys : []).map((key, index) => ({ ref: typeof key === 'string' ? key.trim() : '', index }))
|
|
@@ -89,7 +91,7 @@ export function buildPoolItem({
|
|
|
89
91
|
|
|
90
92
|
const tiersMap = {};
|
|
91
93
|
for (let i = 0; i < refs.length; i++) {
|
|
92
|
-
const val = Array.isArray(tiers) ? tiers[indexed[i].index] : (tiers && typeof tiers ===
|
|
94
|
+
const val = Array.isArray(tiers) ? tiers[indexed[i].index] : (tiers && typeof tiers === 'object' ? tiers[refs[i]] : undefined);
|
|
93
95
|
tiersMap[refs[i]] = Number.isFinite(val) && val >= 1 ? Math.floor(val) : 1;
|
|
94
96
|
}
|
|
95
97
|
const weightsMap = {};
|
|
@@ -143,6 +145,12 @@ const weightsMap = {};
|
|
|
143
145
|
maxQueueWaitMs: typeof maxQueueWaitMs === 'number' && Number.isFinite(maxQueueWaitMs) && maxQueueWaitMs >= 0
|
|
144
146
|
? Math.floor(maxQueueWaitMs)
|
|
145
147
|
: 800,
|
|
148
|
+
maxAttempts: typeof maxAttempts === 'number' && Number.isFinite(maxAttempts) && maxAttempts > 0
|
|
149
|
+
? Math.floor(maxAttempts)
|
|
150
|
+
: 0,
|
|
151
|
+
maxDurationMs: typeof maxDurationMs === 'number' && Number.isFinite(maxDurationMs) && maxDurationMs > 0
|
|
152
|
+
? Math.floor(maxDurationMs)
|
|
153
|
+
: 0,
|
|
146
154
|
};
|
|
147
155
|
}
|
|
148
156
|
|
|
@@ -178,13 +186,18 @@ export function buildPools({ cfg = {}, poolState = new Map(), defaultState = ini
|
|
|
178
186
|
const provRpm = typeof p.rpmLimit === 'number' ? p.rpmLimit : rpmLimit;
|
|
179
187
|
const provTpm = typeof p.tpmLimit === 'number' ? p.tpmLimit : tpmLimit;
|
|
180
188
|
const provConcurrency = typeof p.concurrencyLimit === 'number' ? p.concurrencyLimit : concurrencyLimit;
|
|
189
|
+
const poolAttempts = typeof p.maxAttempts === 'number' ? p.maxAttempts : (cfg.maxAttempts ?? 0);
|
|
190
|
+
const poolDuration = typeof p.maxDurationMs === 'number' ? p.maxDurationMs : (cfg.maxDurationMs ?? 0);
|
|
181
191
|
const pool = buildPool(p.provider, p.keys, p.weights, p.paused, p.revoked, poolCooldown, poolMax, p.expiresAt, p.routingStrategy, p.proactiveRateLimitGuard, {
|
|
182
192
|
provider: p.provider,
|
|
183
193
|
model: null,
|
|
184
194
|
labels: p.labels,
|
|
195
|
+
tiers: p.tiers,
|
|
185
196
|
pinnedRef: p.pinnedRef ?? p.useKeyRef,
|
|
186
197
|
trafficSmoothingBurst: p.trafficSmoothingBurst ?? cfg.trafficSmoothingBurst,
|
|
187
198
|
maxQueueWaitMs: p.maxQueueWaitMs ?? cfg.maxQueueWaitMs,
|
|
199
|
+
maxAttempts: poolAttempts,
|
|
200
|
+
maxDurationMs: poolDuration,
|
|
188
201
|
}, provRpm, provTpm, provConcurrency);
|
|
189
202
|
if (pool) {
|
|
190
203
|
for (const ref of pool.refs) {
|
|
@@ -215,6 +228,8 @@ export function buildPools({ cfg = {}, poolState = new Map(), defaultState = ini
|
|
|
215
228
|
pinnedRef: mp.pinnedRef ?? mp.useKeyRef ?? p.pinnedRef ?? p.useKeyRef,
|
|
216
229
|
trafficSmoothingBurst: mp.trafficSmoothingBurst ?? p.trafficSmoothingBurst ?? cfg.trafficSmoothingBurst,
|
|
217
230
|
maxQueueWaitMs: mp.maxQueueWaitMs ?? p.maxQueueWaitMs ?? cfg.maxQueueWaitMs,
|
|
231
|
+
maxAttempts: typeof mp.maxAttempts === 'number' ? mp.maxAttempts : poolAttempts,
|
|
232
|
+
maxDurationMs: typeof mp.maxDurationMs === 'number' ? mp.maxDurationMs : poolDuration,
|
|
218
233
|
}, modelRpm, modelTpm, modelConcurrency);
|
|
219
234
|
if (!mpool) continue;
|
|
220
235
|
mpool.basePool = pool;
|
package/lib/pool.js
CHANGED
|
@@ -468,14 +468,20 @@ export function isSwitchableError(failureOrPayload, switchCodes = new Set(DEFAUL
|
|
|
468
468
|
// and DEFAULT_SWITCH_CODES is exported as an array); normalize before .has().
|
|
469
469
|
if (!(switchCodes instanceof Set)) switchCodes = new Set(Array.isArray(switchCodes) ? switchCodes : DEFAULT_SWITCH_CODES);
|
|
470
470
|
const failure = failureOrPayload.failure ?? failureOrPayload;
|
|
471
|
-
|
|
472
|
-
let
|
|
471
|
+
let status = Number(failure.status ?? failure.statusCode ?? failure.httpStatus ?? 0);
|
|
472
|
+
let rawCode = String(failure.code ?? failure.reason ?? '').trim();
|
|
473
|
+
let code = rawCode.toUpperCase();
|
|
473
474
|
if (code && SOCKET_TRANSPORT_CODES.has(code)) code = 'TRANSPORT';
|
|
474
475
|
const message = String(failure.message ?? failureOrPayload.message ?? '');
|
|
475
476
|
|
|
476
|
-
//
|
|
477
|
+
// Treat numeric failure.code (e.g. code: "429") as HTTP status if status was not set (#545)
|
|
478
|
+
if (!status && /^\d{3}$/.test(rawCode)) {
|
|
479
|
+
status = Number(rawCode);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 1. Direct HTTP status codes (#267: also 408/425 transient, #545: numeric status/code mapped to semantic switch codes)
|
|
477
483
|
if ((status === 429 || status === 408 || status === 425) && (switchCodes.has('RATE_LIMIT') || switchCodes.has('QUOTA') || switchCodes.has('429') || switchCodes.has('TIMEOUT') || switchCodes.has(String(status)))) return true;
|
|
478
|
-
if ((status === 401 || status === 403) && (switchCodes.has('AUTH') || switchCodes.has('401'))) return true;
|
|
484
|
+
if ((status === 401 || status === 403) && (switchCodes.has('AUTH') || switchCodes.has('401') || switchCodes.has('403') || switchCodes.has(String(status)))) return true;
|
|
479
485
|
if ((status >= 500 && status <= 504) && (switchCodes.has('SERVER') || switchCodes.has(String(status)))) return true;
|
|
480
486
|
|
|
481
487
|
// 2. Standard gRPC / cloud error codes
|
package/lib/request-budget.js
CHANGED
|
@@ -22,13 +22,14 @@ export function createRequestBudget(options = {}) {
|
|
|
22
22
|
|
|
23
23
|
const controller = new AbortController();
|
|
24
24
|
let timerId = null;
|
|
25
|
+
let onCallerAbort = null;
|
|
25
26
|
|
|
26
27
|
// Chain caller signal if provided
|
|
27
28
|
if (options.signal) {
|
|
28
29
|
if (options.signal.aborted) {
|
|
29
30
|
controller.abort(options.signal.reason);
|
|
30
31
|
} else {
|
|
31
|
-
|
|
32
|
+
onCallerAbort = () => {
|
|
32
33
|
controller.abort(options.signal.reason);
|
|
33
34
|
cleanup();
|
|
34
35
|
};
|
|
@@ -52,6 +53,12 @@ export function createRequestBudget(options = {}) {
|
|
|
52
53
|
clearTimeout(timerId);
|
|
53
54
|
timerId = null;
|
|
54
55
|
}
|
|
56
|
+
if (options.signal && onCallerAbort) {
|
|
57
|
+
try {
|
|
58
|
+
options.signal.removeEventListener('abort', onCallerAbort);
|
|
59
|
+
} catch {}
|
|
60
|
+
onCallerAbort = null;
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
return {
|
package/lib/resolver.js
CHANGED
|
@@ -121,7 +121,7 @@ export function createResolver({
|
|
|
121
121
|
const strategy = pool.routingStrategy ?? runtime.routingStrategy ?? 'round-robin';
|
|
122
122
|
const sessionId = resolveOpts?.sessionId ?? resolveOpts?.session ?? resolveOpts?.metadata?.sessionId ?? currentSession?.();
|
|
123
123
|
let list = pool.weightedRefs ?? pool.refs;
|
|
124
|
-
if (strategy === 'lowest-latency' || strategy === 'least-loaded' || strategy === 'sticky') {
|
|
124
|
+
if (strategy === 'lowest-latency' || strategy === 'least-loaded' || strategy === 'sticky' || strategy === 'tier-priority') {
|
|
125
125
|
list = sortAttemptList(list, strategy, {
|
|
126
126
|
latencyHistogram,
|
|
127
127
|
concurrencyTracker,
|