@git-stunts/alfred
Advanced tools
+8
-0
@@ -8,2 +8,10 @@ # Changelog | ||
| ## [0.10.4] - 2026-06-24 | ||
| ### Fixed | ||
| - **Timeout cleanup**: `timeout()` now clears the default runtime timer when the | ||
| wrapped operation resolves or rejects before the deadline, while preserving | ||
| injected-clock behavior for deterministic tests. | ||
| ## [0.10.3] - 2026-02-08 | ||
@@ -10,0 +18,0 @@ |
+1
-1
| { | ||
| "name": "@git-stunts/alfred", | ||
| "version": "0.10.3", | ||
| "version": "0.10.4", | ||
| "description": "Production-grade resilience patterns for async ops: retry/backoff+jitter, circuit breaker, bulkhead, timeout.", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
+1
-1
| { | ||
| "name": "@git-stunts/alfred", | ||
| "version": "0.10.3", | ||
| "version": "0.10.4", | ||
| "description": "Production-grade resilience patterns for async ops: retry/backoff+jitter, circuit breaker, bulkhead, timeout.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+68
-27
@@ -22,2 +22,51 @@ /** | ||
| function rejectWithTimeout(context) { | ||
| const { controller, clock, isCompleted, onTimeout, reject, startTime, telemetry, timeoutMs } = | ||
| context; | ||
| if (isCompleted()) { | ||
| return; | ||
| } | ||
| controller.abort(); | ||
| const elapsed = clock.now() - startTime; | ||
| notifyTimeout({ elapsed, onTimeout }); | ||
| emitTimeoutTelemetry({ clock, elapsed, telemetry, timeoutMs }); | ||
| reject(new TimeoutError(timeoutMs, elapsed)); | ||
| } | ||
| function notifyTimeout({ elapsed, onTimeout }) { | ||
| if (onTimeout) { | ||
| try { | ||
| onTimeout(elapsed); | ||
| } catch { | ||
| // Timeout side effects must not replace the TimeoutError result. | ||
| } | ||
| } | ||
| } | ||
| function emitTimeoutTelemetry({ clock, elapsed, telemetry, timeoutMs }) { | ||
| try { | ||
| telemetry.emit({ | ||
| type: 'timeout', | ||
| timestamp: clock.now(), | ||
| timeout: timeoutMs, | ||
| elapsed, | ||
| metrics: { timeouts: 1, failures: 1 }, | ||
| }); | ||
| } catch { | ||
| // Timeout telemetry must not replace the TimeoutError result. | ||
| } | ||
| } | ||
| function scheduleTimeout(context) { | ||
| if (context.usesInjectedClock) { | ||
| context.clock.sleep(context.timeoutMs).then(() => rejectWithTimeout(context)); | ||
| return null; | ||
| } | ||
| return setTimeout(() => rejectWithTimeout(context), context.timeoutMs); | ||
| } | ||
| /** | ||
@@ -57,3 +106,4 @@ * Executes a function with a timeout limit. | ||
| export async function timeout(ms, fn, options = {}) { | ||
| const { onTimeout, telemetry = new NoopSink(), clock = new SystemClock() } = options; | ||
| const { onTimeout, telemetry = new NoopSink() } = options; | ||
| const clock = options.clock || new SystemClock(); | ||
| const timeoutMs = resolve(ms); | ||
@@ -64,25 +114,15 @@ const controller = new AbortController(); | ||
| let completed = false; | ||
| let timeoutHandle = null; | ||
| const timeoutPromise = new Promise((_, reject) => { | ||
| clock.sleep(timeoutMs).then(() => { | ||
| if (completed) { | ||
| return; | ||
| } | ||
| controller.abort(); | ||
| const elapsed = clock.now() - startTime; | ||
| if (onTimeout) { | ||
| onTimeout(elapsed); | ||
| } | ||
| telemetry.emit({ | ||
| type: 'timeout', | ||
| timestamp: clock.now(), | ||
| timeout: timeoutMs, | ||
| elapsed, | ||
| metrics: { timeouts: 1, failures: 1 }, | ||
| }); | ||
| reject(new TimeoutError(timeoutMs, elapsed)); | ||
| timeoutHandle = scheduleTimeout({ | ||
| controller, | ||
| clock, | ||
| isCompleted: () => completed, | ||
| onTimeout, | ||
| reject, | ||
| startTime, | ||
| telemetry, | ||
| timeoutMs, | ||
| usesInjectedClock: Boolean(options.clock), | ||
| }); | ||
@@ -96,9 +136,10 @@ }); | ||
| const result = await Promise.race([operationPromise, timeoutPromise]); | ||
| return await Promise.race([operationPromise, timeoutPromise]); | ||
| } finally { | ||
| completed = true; | ||
| return result; | ||
| } catch (error) { | ||
| completed = true; | ||
| throw error; | ||
| if (timeoutHandle !== null) { | ||
| clearTimeout(timeoutHandle); | ||
| timeoutHandle = null; | ||
| } | ||
| } | ||
| } |
134374
0.99%2798
1.34%