@loonylabs/tti-middleware 1.8.0 → 1.9.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.
|
@@ -102,10 +102,20 @@ export declare abstract class BaseTTIProvider implements ITTIProvider {
|
|
|
102
102
|
*/
|
|
103
103
|
protected sleep(ms: number): Promise<void>;
|
|
104
104
|
/**
|
|
105
|
-
* Wrap an operation with a timeout
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
105
|
+
* Wrap an operation with a timeout and optional grace period.
|
|
106
|
+
*
|
|
107
|
+
* Normal flow (graceMs = 0):
|
|
108
|
+
* If the operation doesn't resolve within `timeoutMs`, the returned promise
|
|
109
|
+
* rejects immediately with a timeout error.
|
|
110
|
+
*
|
|
111
|
+
* Grace period flow (graceMs > 0):
|
|
112
|
+
* When `timeoutMs` fires, instead of rejecting immediately, a grace period
|
|
113
|
+
* starts. If the operation resolves successfully within `graceMs`, the result
|
|
114
|
+
* is used and no timeout error is thrown. Only if the grace period also expires
|
|
115
|
+
* does the promise reject — with an error reflecting the total wait time.
|
|
116
|
+
*
|
|
117
|
+
* This prevents paying for (and discarding) a Vertex AI response that arrived
|
|
118
|
+
* slightly after the timeout threshold.
|
|
109
119
|
*/
|
|
110
120
|
private withTimeout;
|
|
111
121
|
/**
|
|
@@ -249,6 +249,7 @@ class BaseTTIProvider {
|
|
|
249
249
|
jitter: retryOption.jitter ?? types_1.DEFAULT_RETRY_OPTIONS.jitter,
|
|
250
250
|
timeoutMs: retryOption.timeoutMs ?? types_1.DEFAULT_RETRY_OPTIONS.timeoutMs,
|
|
251
251
|
timeoutRetries: retryOption.timeoutRetries ?? types_1.DEFAULT_RETRY_OPTIONS.timeoutRetries,
|
|
252
|
+
graceMs: retryOption.graceMs ?? types_1.DEFAULT_RETRY_OPTIONS.graceMs,
|
|
252
253
|
};
|
|
253
254
|
}
|
|
254
255
|
/**
|
|
@@ -274,25 +275,68 @@ class BaseTTIProvider {
|
|
|
274
275
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
275
276
|
}
|
|
276
277
|
/**
|
|
277
|
-
* Wrap an operation with a timeout
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
278
|
+
* Wrap an operation with a timeout and optional grace period.
|
|
279
|
+
*
|
|
280
|
+
* Normal flow (graceMs = 0):
|
|
281
|
+
* If the operation doesn't resolve within `timeoutMs`, the returned promise
|
|
282
|
+
* rejects immediately with a timeout error.
|
|
283
|
+
*
|
|
284
|
+
* Grace period flow (graceMs > 0):
|
|
285
|
+
* When `timeoutMs` fires, instead of rejecting immediately, a grace period
|
|
286
|
+
* starts. If the operation resolves successfully within `graceMs`, the result
|
|
287
|
+
* is used and no timeout error is thrown. Only if the grace period also expires
|
|
288
|
+
* does the promise reject — with an error reflecting the total wait time.
|
|
289
|
+
*
|
|
290
|
+
* This prevents paying for (and discarding) a Vertex AI response that arrived
|
|
291
|
+
* slightly after the timeout threshold.
|
|
281
292
|
*/
|
|
282
|
-
withTimeout(operation, timeoutMs, operationName) {
|
|
293
|
+
withTimeout(operation, timeoutMs, graceMs, operationName) {
|
|
283
294
|
return new Promise((resolve, reject) => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
295
|
+
let settled = false;
|
|
296
|
+
let inGracePeriod = false;
|
|
297
|
+
let mainTimerRef;
|
|
298
|
+
let graceTimerRef = null;
|
|
299
|
+
const operationPromise = operation();
|
|
300
|
+
// Handle operation resolution — can fire at any point, including during grace
|
|
301
|
+
operationPromise.then((result) => {
|
|
302
|
+
if (settled)
|
|
303
|
+
return;
|
|
304
|
+
settled = true;
|
|
305
|
+
clearTimeout(mainTimerRef);
|
|
306
|
+
if (graceTimerRef)
|
|
307
|
+
clearTimeout(graceTimerRef);
|
|
308
|
+
if (inGracePeriod) {
|
|
309
|
+
this.log('info', `${operationName} completed during grace period`, { graceMs });
|
|
310
|
+
}
|
|
290
311
|
resolve(result);
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
|
|
312
|
+
}, (error) => {
|
|
313
|
+
if (settled)
|
|
314
|
+
return;
|
|
315
|
+
settled = true;
|
|
316
|
+
clearTimeout(mainTimerRef);
|
|
317
|
+
if (graceTimerRef)
|
|
318
|
+
clearTimeout(graceTimerRef);
|
|
294
319
|
reject(error);
|
|
295
320
|
});
|
|
321
|
+
// Primary timeout
|
|
322
|
+
mainTimerRef = setTimeout(() => {
|
|
323
|
+
if (settled)
|
|
324
|
+
return;
|
|
325
|
+
if (graceMs > 0) {
|
|
326
|
+
inGracePeriod = true;
|
|
327
|
+
this.log('warn', `${operationName} primary timeout after ${timeoutMs}ms, entering grace period (${graceMs}ms)`, { timeoutMs, graceMs });
|
|
328
|
+
graceTimerRef = setTimeout(() => {
|
|
329
|
+
if (settled)
|
|
330
|
+
return;
|
|
331
|
+
settled = true;
|
|
332
|
+
reject(new Error(`timeout: ${operationName} did not complete within ${timeoutMs + graceMs}ms (including ${graceMs}ms grace period)`));
|
|
333
|
+
}, graceMs);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
settled = true;
|
|
337
|
+
reject(new Error(`timeout: ${operationName} did not complete within ${timeoutMs}ms`));
|
|
338
|
+
}
|
|
339
|
+
}, timeoutMs);
|
|
296
340
|
});
|
|
297
341
|
}
|
|
298
342
|
/**
|
|
@@ -336,6 +380,7 @@ class BaseTTIProvider {
|
|
|
336
380
|
return operation();
|
|
337
381
|
}
|
|
338
382
|
const timeoutMs = retryConfig.timeoutMs || 0;
|
|
383
|
+
const graceMs = retryConfig.graceMs ?? 0;
|
|
339
384
|
const maxTimeoutRetries = retryConfig.timeoutRetries ?? 2;
|
|
340
385
|
let lastError = null;
|
|
341
386
|
let generalRetryCount = 0;
|
|
@@ -354,7 +399,7 @@ class BaseTTIProvider {
|
|
|
354
399
|
});
|
|
355
400
|
// Wrap with timeout if configured
|
|
356
401
|
const result = timeoutMs > 0
|
|
357
|
-
? await this.withTimeout(operation, timeoutMs, operationName)
|
|
402
|
+
? await this.withTimeout(operation, timeoutMs, graceMs, operationName)
|
|
358
403
|
: await operation();
|
|
359
404
|
const duration = Date.now() - attemptStart;
|
|
360
405
|
this.log('info', `${operationName} completed in ${duration}ms`, {
|
|
@@ -235,6 +235,23 @@ export interface RetryOptions {
|
|
|
235
235
|
* long waits, while still allowing many retries for quota errors.
|
|
236
236
|
*/
|
|
237
237
|
timeoutRetries?: number;
|
|
238
|
+
/**
|
|
239
|
+
* Grace period in milliseconds after a timeout before abandoning the attempt (default: 0).
|
|
240
|
+
*
|
|
241
|
+
* When `timeoutMs` fires, instead of immediately failing and starting a new retry,
|
|
242
|
+
* the middleware waits an additional `graceMs`. If the in-flight request resolves
|
|
243
|
+
* successfully within this window, the result is used and no retry is needed.
|
|
244
|
+
*
|
|
245
|
+
* This prevents paying for (and discarding) a successful response that arrived
|
|
246
|
+
* slightly after the timeout threshold — which is common under quota pressure when
|
|
247
|
+
* Vertex AI eventually returns a valid result after a long backoff.
|
|
248
|
+
*
|
|
249
|
+
* Example: `timeoutMs: 210000, graceMs: 60000`
|
|
250
|
+
* → waits up to 210s normally, then up to 60s more to capture a late success.
|
|
251
|
+
*
|
|
252
|
+
* Set to 0 (default) to disable — timeout retries fire immediately as before.
|
|
253
|
+
*/
|
|
254
|
+
graceMs?: number;
|
|
238
255
|
/**
|
|
239
256
|
* @deprecated Use `backoffMultiplier` instead. Will be removed in v2.0.
|
|
240
257
|
* When true, equivalent to backoffMultiplier of 1.0 with linear scaling (delayMs * attempt).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loonylabs/tti-middleware",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Provider-agnostic Text-to-Image middleware with GDPR compliance. Supports Google Cloud (Imagen, Gemini), Eden AI, and IONOS.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|