performance-helpers 1.0.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.
- package/.eslintrc.cjs +22 -0
- package/.nojekyll +0 -0
- package/.prettierrc +6 -0
- package/CONTRIBUTING.md +178 -0
- package/LICENSE.md +21 -0
- package/README.md +194 -0
- package/assets/1_Caching.md +4 -0
- package/assets/2_Parallelizing.md +18 -0
- package/assets/3_Logging.md +3 -0
- package/assets/404.md +3 -0
- package/assets/4_Utils.md +10 -0
- package/assets/logo.png +0 -0
- package/assets/navigation.md +10 -0
- package/bench/README.md +97 -0
- package/bench/results.json +94 -0
- package/bench/results.md +233 -0
- package/bench/run.js +2639 -0
- package/bench/worker.js +43 -0
- package/docs/README.md +38 -0
- package/docs/docs-typedoc.json +38714 -0
- package/docs/helpers/constants/README.md +34 -0
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_BACKOFF_MAX_MULTIPLIER.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_COOLDOWN_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_INTERVAL_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_MIN_INTERVAL_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_BACKPRESSURE_QUEUE_CAPACITY.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_BACKPRESSURE_REFILL_INTERVAL_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_BATCH_MAX_SIZE.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_CACHE_DEFAULT_TTL_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_CACHE_MAX_POOL_SIZE.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_CACHE_MAX_WEIGHT_BYTES.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_HISTOGRAM_BUCKET_COUNT.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_HISTOGRAM_MAX_VALUE.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_MAX_CLEANUP_PER_TICK.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_QUEUE_CAPACITY.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_REAPER_MIN_INTERVAL_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_REFILL_INTERVAL_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_RETRY_BASE_DELAY_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_RETRY_MAX_DELAY_MS.md +9 -0
- package/docs/helpers/constants/variables/DEFAULT_TIMEOUT_MS.md +9 -0
- package/docs/helpers/constants/variables/ENCODE_CACHE_LARGE_KEY_LENGTH.md +9 -0
- package/docs/helpers/constants/variables/MAX_DEEP_EQUAL_DEPTH.md +9 -0
- package/docs/helpers/constants/variables/MS_PER_MIN.md +9 -0
- package/docs/helpers/constants/variables/MS_PER_SEC.md +9 -0
- package/docs/helpers/constants/variables/default.md +103 -0
- package/docs/helpers/jsdoc-types/README.md +33 -0
- package/docs/helpers/jsdoc-types/interfaces/BufferDecoder.md +23 -0
- package/docs/helpers/jsdoc-types/interfaces/BufferEncoder.md +23 -0
- package/docs/helpers/jsdoc-types/interfaces/CacheNode.md +43 -0
- package/docs/helpers/jsdoc-types/interfaces/CommonPoolOptions.md +31 -0
- package/docs/helpers/jsdoc-types/interfaces/PendingResponseEntry.md +51 -0
- package/docs/helpers/jsdoc-types/interfaces/PostMessageOptions.md +39 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerBatchOptions.md +13 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerCacheOptions.md +115 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerChunkingOptions.md +31 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerCircuitOptions.md +45 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerDeadlineOptions.md +101 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerDeferOptions.md +13 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerEventBusOptions.md +19 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerLatchOptions.md +23 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerLoggerOptions.md +51 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerObserverOptions.md +25 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerPoolOptions.md +85 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerQueueOptions.md +13 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerRetryOptions.md +83 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerSlidingWindowOptions.md +19 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerTTLMapOptions.md +27 -0
- package/docs/helpers/jsdoc-types/interfaces/PowerThrottleOptions.md +31 -0
- package/docs/helpers/jsdoc-types/interfaces/WorkerObj.md +55 -0
- package/docs/helpers/powerBackpressure/README.md +17 -0
- package/docs/helpers/powerBackpressure/classes/PowerBackpressure.md +368 -0
- package/docs/helpers/powerBatch/README.md +17 -0
- package/docs/helpers/powerBatch/classes/PowerBatch.md +139 -0
- package/docs/helpers/powerBuffer/README.md +26 -0
- package/docs/helpers/powerBuffer/functions/b2o.md +25 -0
- package/docs/helpers/powerBuffer/functions/o2b.md +23 -0
- package/docs/helpers/powerBuffer/functions/o2u8.md +33 -0
- package/docs/helpers/powerBuffer/functions/u82o.md +30 -0
- package/docs/helpers/powerBulkhead/README.md +17 -0
- package/docs/helpers/powerBulkhead/classes/PowerBulkhead.md +302 -0
- package/docs/helpers/powerCache/README.md +29 -0
- package/docs/helpers/powerCache/classes/PowerCache.md +933 -0
- package/docs/helpers/powerCache/classes/PowerMemoizer.md +244 -0
- package/docs/helpers/powerCache/classes/PowerTimedCache.md +302 -0
- package/docs/helpers/powerCache/functions/simpleArgsKey.md +31 -0
- package/docs/helpers/powerChunking/README.md +17 -0
- package/docs/helpers/powerChunking/classes/PowerChunker.md +78 -0
- package/docs/helpers/powerCircuit/README.md +23 -0
- package/docs/helpers/powerCircuit/classes/PowerCircuit.md +167 -0
- package/docs/helpers/powerDeadline/README.md +23 -0
- package/docs/helpers/powerDeadline/classes/PowerDeadline.md +88 -0
- package/docs/helpers/powerDefer/README.md +17 -0
- package/docs/helpers/powerDefer/classes/PowerDefer.md +134 -0
- package/docs/helpers/powerEventBus/README.md +23 -0
- package/docs/helpers/powerEventBus/classes/PowerEventBus.md +330 -0
- package/docs/helpers/powerHistogram/README.md +17 -0
- package/docs/helpers/powerHistogram/classes/PowerHistogram.md +285 -0
- package/docs/helpers/powerLatch/README.md +17 -0
- package/docs/helpers/powerLatch/classes/PowerLatch.md +264 -0
- package/docs/helpers/powerLogger/README.md +17 -0
- package/docs/helpers/powerLogger/classes/PowerLogger.md +290 -0
- package/docs/helpers/powerObserver/README.md +23 -0
- package/docs/helpers/powerObserver/classes/PowerObserver.md +213 -0
- package/docs/helpers/powerPermitGate/README.md +11 -0
- package/docs/helpers/powerPermitGate/classes/PowerPermitGate.md +248 -0
- package/docs/helpers/powerPool/README.md +36 -0
- package/docs/helpers/powerPool/classes/PowerPool.md +973 -0
- package/docs/helpers/powerPool/classes/PowerPoolShutdownError.md +67 -0
- package/docs/helpers/powerQueue/README.md +11 -0
- package/docs/helpers/powerQueue/classes/PowerQueue.md +302 -0
- package/docs/helpers/powerRateLimit/README.md +17 -0
- package/docs/helpers/powerRateLimit/classes/PowerRateLimit.md +187 -0
- package/docs/helpers/powerRetry/README.md +23 -0
- package/docs/helpers/powerRetry/classes/PowerRetry.md +106 -0
- package/docs/helpers/powerScheduler/README.md +11 -0
- package/docs/helpers/powerScheduler/classes/PowerScheduler.md +135 -0
- package/docs/helpers/powerSemaphore/README.md +17 -0
- package/docs/helpers/powerSemaphore/classes/PowerSemaphore.md +173 -0
- package/docs/helpers/powerSlidingWindow/README.md +11 -0
- package/docs/helpers/powerSlidingWindow/classes/PowerSlidingWindow.md +83 -0
- package/docs/helpers/powerSubscriberSet/README.md +15 -0
- package/docs/helpers/powerSubscriberSet/classes/PowerSubscriberSet.md +251 -0
- package/docs/helpers/powerSubscriberSet/functions/cleanupWeakRefs.md +21 -0
- package/docs/helpers/powerTTLMap/README.md +17 -0
- package/docs/helpers/powerTTLMap/classes/PowerTTLMap.md +326 -0
- package/docs/helpers/powerThrottle/README.md +17 -0
- package/docs/helpers/powerThrottle/classes/PowerThrottle.md +216 -0
- package/docs/index/README.md +205 -0
- package/docs/utils/errors/README.md +12 -0
- package/docs/utils/errors/functions/formatErrorObj.md +30 -0
- package/docs/utils/errors/functions/normalizeError.md +50 -0
- package/docs/utils/now/README.md +19 -0
- package/docs/utils/now/functions/measureAsync.md +37 -0
- package/docs/utils/now/functions/measureSync.md +54 -0
- package/docs/utils/now/functions/nowMs.md +24 -0
- package/guides/autoscale.md +80 -0
- package/guides/errors.md +41 -0
- package/guides/metaGuide.md +440 -0
- package/guides/now.md +56 -0
- package/guides/powerBackpressure.md +110 -0
- package/guides/powerBatch.md +82 -0
- package/guides/powerBuffer.md +86 -0
- package/guides/powerBulkhead.md +61 -0
- package/guides/powerCache.md +269 -0
- package/guides/powerChunking.md +130 -0
- package/guides/powerCircuit.md +84 -0
- package/guides/powerDeadline.md +99 -0
- package/guides/powerDefer.md +56 -0
- package/guides/powerEventBus.md +89 -0
- package/guides/powerHistogram.md +71 -0
- package/guides/powerLatch.md +94 -0
- package/guides/powerLogger.md +129 -0
- package/guides/powerObserver.md +65 -0
- package/guides/powerPermitGate.md +52 -0
- package/guides/powerPool.md +321 -0
- package/guides/powerQueue.md +112 -0
- package/guides/powerRateLimit.md +37 -0
- package/guides/powerRetry.md +54 -0
- package/guides/powerScheduler.md +41 -0
- package/guides/powerSemaphore.md +65 -0
- package/guides/powerSlidingWindow.md +63 -0
- package/guides/powerSubscriberSet.md +48 -0
- package/guides/powerTTLMap.md +58 -0
- package/guides/powerThrottle.md +152 -0
- package/index.html +57 -0
- package/package.json +81 -0
- package/results.json +6692 -0
- package/scripts/find-missing-jsdoc.js +62 -0
- package/scripts/modernize-optional-chaining.cjs +36 -0
- package/scripts/pool-debug.mjs +29 -0
- package/scripts/repro_powercache.js +14 -0
- package/scripts/static-audit-exports.cjs +93 -0
- package/scripts/static-audit-exports.json +518 -0
- package/src/helpers/constants.js +69 -0
- package/src/helpers/jsdoc-types.js +221 -0
- package/src/helpers/powerBackpressure.js +182 -0
- package/src/helpers/powerBatch.js +161 -0
- package/src/helpers/powerBuffer.js +161 -0
- package/src/helpers/powerBulkhead.js +203 -0
- package/src/helpers/powerCache.js +1740 -0
- package/src/helpers/powerChunking.js +340 -0
- package/src/helpers/powerCircuit.js +160 -0
- package/src/helpers/powerDeadline.js +181 -0
- package/src/helpers/powerDefer.js +100 -0
- package/src/helpers/powerEventBus.js +405 -0
- package/src/helpers/powerHistogram.js +174 -0
- package/src/helpers/powerLatch.js +247 -0
- package/src/helpers/powerLogger.js +348 -0
- package/src/helpers/powerObserver.js +146 -0
- package/src/helpers/powerPermitGate.js +142 -0
- package/src/helpers/powerPool.js +2793 -0
- package/src/helpers/powerQueue.js +253 -0
- package/src/helpers/powerRateLimit.js +273 -0
- package/src/helpers/powerRetry.js +114 -0
- package/src/helpers/powerScheduler.js +91 -0
- package/src/helpers/powerSemaphore.js +94 -0
- package/src/helpers/powerSlidingWindow.js +86 -0
- package/src/helpers/powerSubscriberSet.js +260 -0
- package/src/helpers/powerTTLMap.js +253 -0
- package/src/helpers/powerThrottle.js +171 -0
- package/src/index.js +27 -0
- package/src/utils/errors.js +45 -0
- package/src/utils/now.js +129 -0
- package/test/powerBackpressure.test.js +114 -0
- package/test/powerBatch.branches.extra.test.js +122 -0
- package/test/powerBatch.test.js +79 -0
- package/test/powerBuffer.test.js +125 -0
- package/test/powerBulkhead.test.js +210 -0
- package/test/powerCache.branches.test.js +233 -0
- package/test/powerCache.bulk.test.js +31 -0
- package/test/powerCache.getorset.test.js +110 -0
- package/test/powerCache.hitRate.test.js +35 -0
- package/test/powerCache.inflight.test.js +24 -0
- package/test/powerCache.iterator.test.js +18 -0
- package/test/powerCache.misses.test.js +52 -0
- package/test/powerCache.more.test.js +118 -0
- package/test/powerCache.test.js +37 -0
- package/test/powerCache.timeout.test.js +25 -0
- package/test/powerCache.touch.test.js +46 -0
- package/test/powerChunking.branches.extra.test.js +155 -0
- package/test/powerChunking.errors.test.js +177 -0
- package/test/powerChunking.test.js +39 -0
- package/test/powerCircuit.observability.test.js +71 -0
- package/test/powerCircuit.test.js +74 -0
- package/test/powerDeadline.test.js +140 -0
- package/test/powerDefer.test.js +55 -0
- package/test/powerErrors.test.js +32 -0
- package/test/powerEventBus.branches.extra.test.js +70 -0
- package/test/powerEventBus.extra.test.js +72 -0
- package/test/powerEventBus.max.test.js +43 -0
- package/test/powerEventBus.more.test.js +121 -0
- package/test/powerEventBus.once_off.test.js +17 -0
- package/test/powerEventBus.test.js +74 -0
- package/test/powerEventBus.uncovered.test.js +57 -0
- package/test/powerEventBus.weak.test.js +18 -0
- package/test/powerHistogram.test.js +73 -0
- package/test/powerLatch.branches.extra.test.js +115 -0
- package/test/powerLatch.test.js +57 -0
- package/test/powerLogger.branches.test.js +98 -0
- package/test/powerLogger.formatter.name.test.js +58 -0
- package/test/powerLogger.json.test.js +88 -0
- package/test/powerLogger.output.test.js +81 -0
- package/test/powerLogger.table.debug.test.js +77 -0
- package/test/powerLogger.test.js +59 -0
- package/test/powerMemoizer.memoize.test.js +100 -0
- package/test/powerMemoizer.test.js +85 -0
- package/test/powerObserver.test.js +129 -0
- package/test/powerPermitGate.test.js +66 -0
- package/test/powerPool.autoTransfer.test.js +100 -0
- package/test/powerPool.autoscale.extra.test.js +88 -0
- package/test/powerPool.autoscale.test.js +136 -0
- package/test/powerPool.awaitDefaultTimeout.test.js +52 -0
- package/test/powerPool.awaitTimeout.test.js +22 -0
- package/test/powerPool.batch.test.js +170 -0
- package/test/powerPool.branches.extra2.test.js +42 -0
- package/test/powerPool.branches.test.js +102 -0
- package/test/powerPool.browser.messageerror.test.js +45 -0
- package/test/powerPool.correlation.test.js +26 -0
- package/test/powerPool.correlationId.test.js +63 -0
- package/test/powerPool.dispose.test.js +49 -0
- package/test/powerPool.drain.test.js +57 -0
- package/test/powerPool.events.test.js +131 -0
- package/test/powerPool.more.extra.test.js +99 -0
- package/test/powerPool.more.test.js +283 -0
- package/test/powerPool.node.messageerror.test.js +46 -0
- package/test/powerPool.postMessage.promise.test.js +83 -0
- package/test/powerPool.queueHigh.test.js +55 -0
- package/test/powerPool.queueSaturation.test.js +51 -0
- package/test/powerPool.rapidResize.test.js +55 -0
- package/test/powerPool.resize.overload.test.js +65 -0
- package/test/powerPool.resize.test.js +70 -0
- package/test/powerPool.shutdown.test.js +38 -0
- package/test/powerPool.stats.test.js +40 -0
- package/test/powerPool.stopThePress.test.js +94 -0
- package/test/powerPool.terminateShutdown.test.js +22 -0
- package/test/powerPool.test.js +525 -0
- package/test/powerPool.timers.test.js +55 -0
- package/test/powerPool.uncovered.test.js +407 -0
- package/test/powerPool.workerId.test.js +47 -0
- package/test/powerQueue.iterators.test.js +67 -0
- package/test/powerQueue.saturation.test.js +18 -0
- package/test/powerQueue.test.js +48 -0
- package/test/powerQueue.unshiftMany.test.js +49 -0
- package/test/powerRateLimit.atomic.test.js +80 -0
- package/test/powerRateLimit.extra.test.js +145 -0
- package/test/powerRateLimit.functions.test.js +106 -0
- package/test/powerRateLimit.test.js +38 -0
- package/test/powerRetry.attemptTimeout.test.js +51 -0
- package/test/powerRetry.test.js +121 -0
- package/test/powerScheduler.test.js +126 -0
- package/test/powerSemaphore.test.js +108 -0
- package/test/powerSlidingWindow.pool.test.js +55 -0
- package/test/powerSlidingWindow.test.js +25 -0
- package/test/powerSubscriberSet.test.js +125 -0
- package/test/powerTTLMap.test.js +125 -0
- package/test/powerThrottle.pool.test.js +54 -0
- package/test/powerThrottle.refill.test.js +22 -0
- package/test/powerThrottle.reserve.test.js +46 -0
- package/test/powerThrottle.test.js +45 -0
- package/test/powerTimedCache.test.js +73 -0
- package/test/umd.bundle.branches.test.js +100 -0
- package/test/umd.bundle.cache-timers.test.js +48 -0
- package/test/umd.bundle.exhaustive.test.js +158 -0
- package/test/umd.bundle.fuzz.test.js +86 -0
- package/test/umd.bundle.hasEqual.more.test.js +68 -0
- package/test/umd.bundle.hasEqual.test.js +104 -0
- package/test/umd.bundle.logger-extra.test.js +48 -0
- package/test/umd.bundle.more-coverage-2.test.js +67 -0
- package/test/umd.bundle.pool.test.js +134 -0
- package/test/umd.bundle.test.js +265 -0
- package/test/utils.measure.test.js +49 -0
- package/test/utils.now.extra.test.js +30 -0
- package/test/utils.now.more.test.js +57 -0
- package/tsconfig.json +16 -0
- package/typedoc.json +25 -0
- package/types/helpers/constants.d.ts +49 -0
- package/types/helpers/jsdoc-types.d.ts +250 -0
- package/types/helpers/powerBackpressure.d.ts +40 -0
- package/types/helpers/powerBatch.d.ts +73 -0
- package/types/helpers/powerBuffer.d.ts +6 -0
- package/types/helpers/powerBulkhead.d.ts +78 -0
- package/types/helpers/powerCache.d.ts +613 -0
- package/types/helpers/powerChunking.d.ts +41 -0
- package/types/helpers/powerCircuit.d.ts +43 -0
- package/types/helpers/powerDeadline.d.ts +36 -0
- package/types/helpers/powerDefer.d.ts +47 -0
- package/types/helpers/powerEventBus.d.ts +108 -0
- package/types/helpers/powerHistogram.d.ts +57 -0
- package/types/helpers/powerLatch.d.ts +89 -0
- package/types/helpers/powerLogger.d.ts +139 -0
- package/types/helpers/powerObserver.d.ts +44 -0
- package/types/helpers/powerPermitGate.d.ts +68 -0
- package/types/helpers/powerPool.d.ts +555 -0
- package/types/helpers/powerQueue.d.ts +126 -0
- package/types/helpers/powerRateLimit.d.ts +90 -0
- package/types/helpers/powerRetry.d.ts +35 -0
- package/types/helpers/powerScheduler.d.ts +44 -0
- package/types/helpers/powerSemaphore.d.ts +44 -0
- package/types/helpers/powerSlidingWindow.d.ts +44 -0
- package/types/helpers/powerSubscriberSet.d.ts +78 -0
- package/types/helpers/powerTTLMap.d.ts +108 -0
- package/types/helpers/powerThrottle.d.ts +86 -0
- package/types/index.d.ts +26 -0
- package/types/utils/errors.d.ts +30 -0
- package/types/utils/now.d.ts +45 -0
- package/vite.config.js +31 -0
- package/vitest.config.js +17 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PowerRateLimit } from '../src/helpers/powerRateLimit.js';
|
|
3
|
+
|
|
4
|
+
describe('PowerRateLimit atomic semantics', () => {
|
|
5
|
+
it('rolls back prior reservations when a later limiter fails', () => {
|
|
6
|
+
let released = false;
|
|
7
|
+
const okLimiter = {
|
|
8
|
+
reserve: (n) => ({ reserved: n }),
|
|
9
|
+
release: (token) => {
|
|
10
|
+
if (token && token.reserved) released = true;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const failingLimiter = {
|
|
15
|
+
reserve: () => null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const pr = new PowerRateLimit([okLimiter, failingLimiter], { atomic: true });
|
|
19
|
+
const ok = pr.tryConsume(1, { atomic: true });
|
|
20
|
+
expect(ok).toBe(false);
|
|
21
|
+
expect(released).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Fake limiter without available(), but supporting tryConsume and addTokens (undo)
|
|
26
|
+
class FakeUndoLimiter {
|
|
27
|
+
constructor(tokens = 0) {
|
|
28
|
+
this.tokens = tokens;
|
|
29
|
+
}
|
|
30
|
+
tryConsume(n) {
|
|
31
|
+
if (this.tokens >= n) {
|
|
32
|
+
this.tokens -= n;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
addTokens(n) {
|
|
38
|
+
this.tokens += n;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fake limiter without available() and without undo support
|
|
43
|
+
class FakeNoUndoLimiter {
|
|
44
|
+
constructor(tokens = 0) {
|
|
45
|
+
this.tokens = tokens;
|
|
46
|
+
}
|
|
47
|
+
tryConsume(n) {
|
|
48
|
+
if (this.tokens >= n) {
|
|
49
|
+
this.tokens -= n;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('PowerRateLimit atomic semantics', () => {
|
|
57
|
+
it('rolls back prior commits when a later limiter fails and supports undo', () => {
|
|
58
|
+
const a = new FakeUndoLimiter(1);
|
|
59
|
+
const b = new FakeNoUndoLimiter(0);
|
|
60
|
+
const r = new PowerRateLimit([a, b], { atomic: true });
|
|
61
|
+
|
|
62
|
+
const ok = r.tryConsume(1);
|
|
63
|
+
expect(ok).toBe(false);
|
|
64
|
+
// a should have been rolled back to 1 token
|
|
65
|
+
expect(a.tokens).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('refuses atomic consume when a limiter lacks undo capability', () => {
|
|
69
|
+
const a = new FakeNoUndoLimiter(1);
|
|
70
|
+
const b = new FakeNoUndoLimiter(1);
|
|
71
|
+
const r = new PowerRateLimit([a, b], { atomic: true });
|
|
72
|
+
|
|
73
|
+
// both have tokens=1 but they lack available() and undo, so atomic cannot be guaranteed
|
|
74
|
+
const ok = r.tryConsume(1);
|
|
75
|
+
expect(ok).toBe(false);
|
|
76
|
+
// ensure no side-effects
|
|
77
|
+
expect(a.tokens).toBe(1);
|
|
78
|
+
expect(b.tokens).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { PowerRateLimit } from '../src/helpers/powerRateLimit.js';
|
|
3
|
+
|
|
4
|
+
describe('PowerRateLimit extra branches', () => {
|
|
5
|
+
it('constructor requires an array', () => {
|
|
6
|
+
expect(() => new PowerRateLimit({})).toThrow(TypeError);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('tryConsume with 0 returns true', () => {
|
|
10
|
+
const r = new PowerRateLimit([]);
|
|
11
|
+
expect(r.tryConsume(0)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('fast available() check returns false when any limiter reports insufficient', () => {
|
|
15
|
+
const limiter = { available: () => 0, tryConsume: vi.fn(() => true) };
|
|
16
|
+
const r = new PowerRateLimit([limiter]);
|
|
17
|
+
expect(r.tryConsume(1)).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('available() returns the minimum available across child limiters', () => {
|
|
21
|
+
const a = { available: () => 5, tryConsume: vi.fn(() => true) };
|
|
22
|
+
const b = { available: () => 2, tryConsume: vi.fn(() => true) };
|
|
23
|
+
const r = new PowerRateLimit([a, b]);
|
|
24
|
+
expect(r.available()).toBe(2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('available() returns Infinity when no limiters are configured', () => {
|
|
28
|
+
const r = new PowerRateLimit([]);
|
|
29
|
+
expect(r.available()).toBe(Infinity);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('available() returns 0 when any child limiter lacks available()', () => {
|
|
33
|
+
const a = { available: () => 5, tryConsume: vi.fn(() => true) };
|
|
34
|
+
const b = { tryConsume: vi.fn(() => true) };
|
|
35
|
+
const r = new PowerRateLimit([a, b]);
|
|
36
|
+
expect(r.available()).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('reserve and release work across atomic limiters', () => {
|
|
40
|
+
const l1 = { reserve: vi.fn((n) => ({ n })), release: vi.fn() };
|
|
41
|
+
const l2 = { tryConsume: vi.fn(() => true), rollback: vi.fn() };
|
|
42
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
43
|
+
const token = r.reserve(1);
|
|
44
|
+
expect(token).toEqual({ n: 1 });
|
|
45
|
+
expect(l1.reserve).toHaveBeenCalledWith(1);
|
|
46
|
+
r.release(token);
|
|
47
|
+
expect(l1.release).toHaveBeenCalledWith(token);
|
|
48
|
+
expect(l2.rollback).toHaveBeenCalledWith(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rollback aliases release', () => {
|
|
52
|
+
const l1 = { reserve: vi.fn((n) => ({ n })), release: vi.fn() };
|
|
53
|
+
const l2 = { tryConsume: vi.fn(() => true), rollback: vi.fn() };
|
|
54
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
55
|
+
const token = r.reserve(1);
|
|
56
|
+
expect(token).not.toBeNull();
|
|
57
|
+
r.rollback(token);
|
|
58
|
+
expect(l1.release).toHaveBeenCalledWith(token);
|
|
59
|
+
expect(l2.rollback).toHaveBeenCalledWith(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reserve returns null when atomic reservation cannot be guaranteed', () => {
|
|
63
|
+
const a = { reserve: vi.fn((n) => ({ n })), release: vi.fn() };
|
|
64
|
+
const b = { tryConsume: vi.fn(() => false), rollback: vi.fn() };
|
|
65
|
+
const r = new PowerRateLimit([a, b], { atomic: true });
|
|
66
|
+
expect(r.reserve(1)).toBeNull();
|
|
67
|
+
expect(a.release).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reserve returns a zero token for non-positive requests and release ignores zero-ish input', () => {
|
|
71
|
+
const l = { release: vi.fn(), rollback: vi.fn(), addTokens: vi.fn() };
|
|
72
|
+
const r = new PowerRateLimit([l], { atomic: true });
|
|
73
|
+
|
|
74
|
+
expect(r.reserve(0)).toEqual({ n: 0 });
|
|
75
|
+
r.release(null);
|
|
76
|
+
r.release({ n: 0 });
|
|
77
|
+
r.release(0);
|
|
78
|
+
|
|
79
|
+
expect(l.release).not.toHaveBeenCalled();
|
|
80
|
+
expect(l.rollback).not.toHaveBeenCalled();
|
|
81
|
+
expect(l.addTokens).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('release falls back to addTokens when release and rollback are unavailable or throw', () => {
|
|
85
|
+
const limiter = {
|
|
86
|
+
release: vi.fn(() => {
|
|
87
|
+
throw new Error('release failed');
|
|
88
|
+
}),
|
|
89
|
+
rollback: vi.fn(() => {
|
|
90
|
+
throw new Error('rollback failed');
|
|
91
|
+
}),
|
|
92
|
+
addTokens: vi.fn(),
|
|
93
|
+
};
|
|
94
|
+
const r = new PowerRateLimit([limiter]);
|
|
95
|
+
|
|
96
|
+
r.release(2);
|
|
97
|
+
|
|
98
|
+
expect(limiter.release).toHaveBeenCalled();
|
|
99
|
+
expect(limiter.rollback).toHaveBeenCalledWith(2);
|
|
100
|
+
expect(limiter.addTokens).toHaveBeenCalledWith(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('reset swallows limiter reset errors and rollback remains a public alias', () => {
|
|
104
|
+
const limiter = {
|
|
105
|
+
reset: vi.fn(() => {
|
|
106
|
+
throw new Error('reset failed');
|
|
107
|
+
}),
|
|
108
|
+
rollback: vi.fn(),
|
|
109
|
+
};
|
|
110
|
+
const r = new PowerRateLimit([limiter]);
|
|
111
|
+
|
|
112
|
+
expect(() => r.reset()).not.toThrow();
|
|
113
|
+
r.rollback({ n: 3 });
|
|
114
|
+
|
|
115
|
+
expect(limiter.reset).toHaveBeenCalled();
|
|
116
|
+
expect(limiter.rollback).toHaveBeenCalledWith(3);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('atomic consume returns false when a limiter lacks undo capability', () => {
|
|
120
|
+
const a = { available: () => 10, tryConsume: vi.fn(() => true) };
|
|
121
|
+
// b lacks available and any undo primitives
|
|
122
|
+
const b = { tryConsume: vi.fn(() => true) };
|
|
123
|
+
const r = new PowerRateLimit([a, b], { atomic: true });
|
|
124
|
+
expect(r.tryConsume(1)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('reserve + tryConsume commit succeeds and calls reserve', () => {
|
|
128
|
+
const l1 = { reserve: vi.fn(() => ({ t: true })), release: vi.fn() };
|
|
129
|
+
// provide a noop rollback so pre-check allows atomic path
|
|
130
|
+
const l2 = { tryConsume: vi.fn(() => true), rollback: vi.fn() };
|
|
131
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
132
|
+
expect(r.tryConsume(1)).toBe(true);
|
|
133
|
+
expect(l1.reserve).toHaveBeenCalled();
|
|
134
|
+
expect(l2.tryConsume).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('undo is attempted when a later limiter fails', () => {
|
|
138
|
+
const l1 = { reserve: vi.fn(() => ({ t: true })), release: vi.fn() };
|
|
139
|
+
const l2 = { tryConsume: vi.fn(() => false), rollback: vi.fn() };
|
|
140
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
141
|
+
expect(r.tryConsume(1)).toBe(false);
|
|
142
|
+
// release should have been called as part of rollback attempts
|
|
143
|
+
expect(l1.release).toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import PowerRateLimit from '../src/helpers/powerRateLimit.js';
|
|
3
|
+
|
|
4
|
+
describe('PowerRateLimit functions and undo paths', () => {
|
|
5
|
+
it('returns false when available() throws', () => {
|
|
6
|
+
const bad = {
|
|
7
|
+
available: () => {
|
|
8
|
+
throw new Error('boom');
|
|
9
|
+
},
|
|
10
|
+
tryConsume: () => true,
|
|
11
|
+
};
|
|
12
|
+
const r = new PowerRateLimit([bad]);
|
|
13
|
+
expect(r.tryConsume(1)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('throws TypeError when limiter missing tryConsume in fast-path', () => {
|
|
17
|
+
const lim = { available: () => 10 };
|
|
18
|
+
const r = new PowerRateLimit([lim]);
|
|
19
|
+
expect(() => r.tryConsume(1)).toThrow(TypeError);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('rollback calls release when a reserve later throws', () => {
|
|
23
|
+
const l1 = { reserve: () => ({ tok: true }), release: vi.fn() };
|
|
24
|
+
const l2 = {
|
|
25
|
+
reserve: () => {
|
|
26
|
+
throw new Error('reserve fail');
|
|
27
|
+
},
|
|
28
|
+
release: vi.fn(),
|
|
29
|
+
};
|
|
30
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
31
|
+
expect(r.tryConsume(1)).toBe(false);
|
|
32
|
+
expect(l1.release).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('undo uses rollback/addTokens/reset fallback when necessary', () => {
|
|
36
|
+
const l1 = { available: () => 10, tryConsume: () => true, reset: vi.fn() };
|
|
37
|
+
const l2 = { tryConsume: () => false, rollback: vi.fn() };
|
|
38
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
39
|
+
expect(r.tryConsume(1)).toBe(false);
|
|
40
|
+
expect(l1.reset).toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('undo swallows errors from release/rollback', () => {
|
|
44
|
+
const l1 = {
|
|
45
|
+
reserve: () => ({ tok: true }),
|
|
46
|
+
release: () => {
|
|
47
|
+
throw new Error('boom release');
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const l2 = {
|
|
51
|
+
tryConsume: () => {
|
|
52
|
+
throw new Error('boom try');
|
|
53
|
+
},
|
|
54
|
+
rollback: () => {
|
|
55
|
+
throw new Error('boom rollback');
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
59
|
+
// should return false and not throw despite undo errors
|
|
60
|
+
expect(r.tryConsume(1)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('direct _undoCommit release/rollback/addTokens/reset paths are exercised', async () => {
|
|
64
|
+
const r = new PowerRateLimit([]);
|
|
65
|
+
|
|
66
|
+
const rel = { release: vi.fn() };
|
|
67
|
+
await r._undoCommit({ l: rel, method: 'reserve', token: 't' }, 1);
|
|
68
|
+
expect(rel.release).toHaveBeenCalledWith('t');
|
|
69
|
+
|
|
70
|
+
const rb = { rollback: vi.fn() };
|
|
71
|
+
await r._undoCommit({ l: rb, method: 'tryConsume' }, 2);
|
|
72
|
+
expect(rb.rollback).toHaveBeenCalledWith(2);
|
|
73
|
+
|
|
74
|
+
const at = { addTokens: vi.fn() };
|
|
75
|
+
await r._undoCommit({ l: at, method: 'tryConsume' }, 3);
|
|
76
|
+
expect(at.addTokens).toHaveBeenCalledWith(3);
|
|
77
|
+
|
|
78
|
+
const rs = { reset: vi.fn() };
|
|
79
|
+
await r._undoCommit({ l: rs, method: 'tryConsume' }, 4);
|
|
80
|
+
expect(rs.reset).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('throws TypeError and rolls back when atomic fallback limiter missing tryConsume or reserve', () => {
|
|
84
|
+
// first limiter reserves successfully
|
|
85
|
+
const l1 = { reserve: () => ({ t: true }), release: vi.fn() };
|
|
86
|
+
// second limiter lacks reserve and tryConsume but has addTokens (pre-check passes)
|
|
87
|
+
const l2 = { addTokens: () => {} /* no tryConsume or reserve */ };
|
|
88
|
+
const r = new PowerRateLimit([l1, l2], { atomic: true });
|
|
89
|
+
expect(() => r.tryConsume(1)).toThrow(TypeError);
|
|
90
|
+
// ensure rollback attempted for l1
|
|
91
|
+
expect(l1.release).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('calls limiter.reset via _undoCommit and reset()', async () => {
|
|
95
|
+
const l = { reset: vi.fn() };
|
|
96
|
+
const r = new PowerRateLimit([l]);
|
|
97
|
+
// call public reset()
|
|
98
|
+
r.reset();
|
|
99
|
+
expect(l.reset).toHaveBeenCalled();
|
|
100
|
+
|
|
101
|
+
// call private undo path that falls back to reset
|
|
102
|
+
l.reset.mockClear();
|
|
103
|
+
await r._undoCommit({ l, method: 'tryConsume' }, 1);
|
|
104
|
+
expect(l.reset).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PowerThrottle } from '../src/helpers/powerThrottle.js';
|
|
3
|
+
import { PowerSlidingWindow } from '../src/helpers/powerSlidingWindow.js';
|
|
4
|
+
import PowerRateLimit from '../src/helpers/powerRateLimit.js';
|
|
5
|
+
|
|
6
|
+
describe('PowerRateLimit', () => {
|
|
7
|
+
it('succeeds only when all underlying limiters allow', () => {
|
|
8
|
+
const t = new PowerThrottle({ capacity: 1, tokens: 1, refillRate: 0 });
|
|
9
|
+
const w = new PowerSlidingWindow({ capacity: 2, windowMs: 10000 });
|
|
10
|
+
const r = new PowerRateLimit([t, w]);
|
|
11
|
+
|
|
12
|
+
expect(r.tryConsume()).toBe(true);
|
|
13
|
+
// throttle consumed (capacity 1) so next immediate attempt should fail
|
|
14
|
+
expect(r.tryConsume()).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('respects sliding-window when throttle has higher capacity', () => {
|
|
18
|
+
const t = new PowerThrottle({ capacity: 5, tokens: 5, refillRate: 0 });
|
|
19
|
+
const w = new PowerSlidingWindow({ capacity: 1, windowMs: 10000 });
|
|
20
|
+
const r = new PowerRateLimit([t, w]);
|
|
21
|
+
|
|
22
|
+
expect(r.tryConsume()).toBe(true);
|
|
23
|
+
// sliding window capacity is 1, so second immediate attempt is blocked
|
|
24
|
+
expect(r.tryConsume()).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reset resets underlying limiters', () => {
|
|
28
|
+
const t = new PowerThrottle({ capacity: 1, tokens: 0, refillRate: 0 });
|
|
29
|
+
const w = new PowerSlidingWindow({ capacity: 1, windowMs: 10000 });
|
|
30
|
+
const r = new PowerRateLimit([t, w]);
|
|
31
|
+
|
|
32
|
+
// nothing available initially
|
|
33
|
+
expect(r.tryConsume()).toBe(false);
|
|
34
|
+
// reset should restore underlying limiters to default/full state
|
|
35
|
+
r.reset();
|
|
36
|
+
expect(r.tryConsume()).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import PowerRetry from '../src/helpers/powerRetry.js';
|
|
3
|
+
|
|
4
|
+
describe('PowerRetry attemptTimeout', () => {
|
|
5
|
+
it('times out a slow attempt and retries according to maxAttempts', async () => {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
const fn = () => new Promise((res) => setTimeout(res, 1000)); // never resolves within attemptTimeout
|
|
8
|
+
const opts = {
|
|
9
|
+
maxAttempts: 2,
|
|
10
|
+
attemptTimeout: 50,
|
|
11
|
+
baseDelay: 1,
|
|
12
|
+
backoff: 'fixed',
|
|
13
|
+
jitter: false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let thrown = null;
|
|
17
|
+
try {
|
|
18
|
+
await PowerRetry.run(fn, opts);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
thrown = err;
|
|
21
|
+
}
|
|
22
|
+
expect(thrown).toBeTruthy();
|
|
23
|
+
// should have taken at least attemptTimeout * maxAttempts (with tiny delays)
|
|
24
|
+
expect(Date.now() - start).toBeGreaterThanOrEqual(50);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('treats a timed-out attempt as a failed attempt and succeeds on retry', async () => {
|
|
28
|
+
let call = 0;
|
|
29
|
+
const fn = () =>
|
|
30
|
+
new Promise((res) => {
|
|
31
|
+
call += 1;
|
|
32
|
+
if (call === 1) {
|
|
33
|
+
// first attempt stalls
|
|
34
|
+
return; // never resolves
|
|
35
|
+
}
|
|
36
|
+
res('ok');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const opts = {
|
|
40
|
+
maxAttempts: 3,
|
|
41
|
+
attemptTimeout: 30,
|
|
42
|
+
baseDelay: 1,
|
|
43
|
+
backoff: 'fixed',
|
|
44
|
+
jitter: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const res = await PowerRetry.run(fn, opts);
|
|
48
|
+
expect(res).toBe('ok');
|
|
49
|
+
expect(call).toBeGreaterThanOrEqual(2);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PowerRetry } from '../src/helpers/powerRetry.js';
|
|
3
|
+
|
|
4
|
+
describe('PowerRetry', () => {
|
|
5
|
+
it('throws a TypeError when fn is not callable', async () => {
|
|
6
|
+
await expect(PowerRetry.run(null)).rejects.toThrow('fn must be a function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('retries failed attempts up to maxAttempts and succeeds', async () => {
|
|
10
|
+
let calls = 0;
|
|
11
|
+
const fn = async () => {
|
|
12
|
+
calls++;
|
|
13
|
+
if (calls < 3) throw new Error('fail');
|
|
14
|
+
return 'ok';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const res = await PowerRetry.run(fn, { maxAttempts: 4, baseDelay: 1, jitter: false });
|
|
18
|
+
expect(res).toBe('ok');
|
|
19
|
+
expect(calls).toBe(3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('rejects after exhausting attempts', async () => {
|
|
23
|
+
let calls = 0;
|
|
24
|
+
const fn = async () => {
|
|
25
|
+
calls++;
|
|
26
|
+
throw new Error('bad');
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await expect(
|
|
30
|
+
PowerRetry.run(fn, { maxAttempts: 2, baseDelay: 1, jitter: false })
|
|
31
|
+
).rejects.toThrow('bad');
|
|
32
|
+
expect(calls).toBe(2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('honors retryIf predicate', async () => {
|
|
36
|
+
let calls = 0;
|
|
37
|
+
const fn = async () => {
|
|
38
|
+
calls++;
|
|
39
|
+
const e = new Error('oops');
|
|
40
|
+
e.status = 400;
|
|
41
|
+
throw e;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
await expect(
|
|
45
|
+
PowerRetry.run(fn, {
|
|
46
|
+
maxAttempts: 3,
|
|
47
|
+
baseDelay: 1,
|
|
48
|
+
jitter: false,
|
|
49
|
+
retryIf: (err) => err.status >= 500,
|
|
50
|
+
})
|
|
51
|
+
).rejects.toThrow('oops');
|
|
52
|
+
expect(calls).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('supports instance-level default options merged with per-call overrides', async () => {
|
|
56
|
+
let calls = 0;
|
|
57
|
+
const retry = new PowerRetry({ maxAttempts: 4, baseDelay: 1, jitter: false });
|
|
58
|
+
const fn = async () => {
|
|
59
|
+
calls += 1;
|
|
60
|
+
if (calls < 2) throw new Error('retry me');
|
|
61
|
+
return 'ok';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const res = await retry.run(fn, { backoff: 'fixed' });
|
|
65
|
+
expect(res).toBe('ok');
|
|
66
|
+
expect(calls).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('treats boolean retryIf values as fixed retry policy', async () => {
|
|
70
|
+
let calls = 0;
|
|
71
|
+
const fn = async () => {
|
|
72
|
+
calls += 1;
|
|
73
|
+
throw new Error('still bad');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
await expect(
|
|
77
|
+
PowerRetry.run(fn, { maxAttempts: 3, baseDelay: 1, jitter: false, retryIf: false })
|
|
78
|
+
).rejects.toThrow('still bad');
|
|
79
|
+
expect(calls).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('swallows onRetry callback errors and continues retrying', async () => {
|
|
83
|
+
let calls = 0;
|
|
84
|
+
const fn = async () => {
|
|
85
|
+
calls += 1;
|
|
86
|
+
if (calls < 2) throw new Error('transient');
|
|
87
|
+
return 'ok';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const res = await PowerRetry.run(fn, {
|
|
91
|
+
maxAttempts: 2,
|
|
92
|
+
baseDelay: 1,
|
|
93
|
+
jitter: false,
|
|
94
|
+
onRetry() {
|
|
95
|
+
throw new Error('observer failed');
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(res).toBe('ok');
|
|
100
|
+
expect(calls).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('fails fast when maxAttempts is non-positive', async () => {
|
|
104
|
+
let calls = 0;
|
|
105
|
+
const fn = async () => {
|
|
106
|
+
calls += 1;
|
|
107
|
+
throw new Error('bad-attempts');
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await expect(
|
|
111
|
+
PowerRetry.run(fn, { maxAttempts: -3, baseDelay: 1, jitter: false })
|
|
112
|
+
).rejects.toThrow('maxAttempts must be a positive finite number');
|
|
113
|
+
expect(calls).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('fails fast when maxAttempts is not finite', async () => {
|
|
117
|
+
await expect(PowerRetry.run(async () => 'ok', { maxAttempts: Number.NaN })).rejects.toThrow(
|
|
118
|
+
'maxAttempts must be a positive finite number'
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { PowerScheduler } from '../src/helpers/powerScheduler.js';
|
|
3
|
+
|
|
4
|
+
describe('PowerScheduler', () => {
|
|
5
|
+
it('throws when constructed without a flush function', () => {
|
|
6
|
+
expect(() => new PowerScheduler(null)).toThrow(TypeError);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('schedules a microtask flush and invokes the callback once', async () => {
|
|
10
|
+
let called = 0;
|
|
11
|
+
const scheduler = new PowerScheduler(() => {
|
|
12
|
+
called += 1;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
scheduler.schedule();
|
|
16
|
+
scheduler.schedule();
|
|
17
|
+
expect(scheduler.scheduled).toBe(true);
|
|
18
|
+
await Promise.resolve();
|
|
19
|
+
expect(called).toBe(1);
|
|
20
|
+
expect(scheduler.scheduled).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('supports flushing immediately before the scheduled callback runs', async () => {
|
|
24
|
+
let called = 0;
|
|
25
|
+
const scheduler = new PowerScheduler(() => {
|
|
26
|
+
called += 1;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
scheduler.schedule();
|
|
30
|
+
expect(scheduler.scheduled).toBe(true);
|
|
31
|
+
scheduler.flush();
|
|
32
|
+
expect(called).toBe(1);
|
|
33
|
+
expect(scheduler.scheduled).toBe(false);
|
|
34
|
+
await Promise.resolve();
|
|
35
|
+
expect(called).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('cancels a scheduled flush and prevents callback invocation', async () => {
|
|
39
|
+
let called = 0;
|
|
40
|
+
const scheduler = new PowerScheduler(() => {
|
|
41
|
+
called += 1;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
scheduler.schedule();
|
|
45
|
+
expect(scheduler.scheduled).toBe(true);
|
|
46
|
+
scheduler.cancel();
|
|
47
|
+
expect(scheduler.scheduled).toBe(false);
|
|
48
|
+
await Promise.resolve();
|
|
49
|
+
expect(called).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('supports macrotask scheduling', async () => {
|
|
53
|
+
let called = 0;
|
|
54
|
+
const scheduler = new PowerScheduler(
|
|
55
|
+
() => {
|
|
56
|
+
called += 1;
|
|
57
|
+
},
|
|
58
|
+
{ scheduling: 'macrotask' }
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
scheduler.schedule();
|
|
62
|
+
expect(scheduler.scheduled).toBe(true);
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
64
|
+
expect(called).toBe(1);
|
|
65
|
+
expect(scheduler.scheduled).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('flush and cancel are no-ops when nothing is scheduled', () => {
|
|
69
|
+
const scheduler = new PowerScheduler(() => {});
|
|
70
|
+
|
|
71
|
+
expect(() => scheduler.flush()).not.toThrow();
|
|
72
|
+
expect(() => scheduler.cancel()).not.toThrow();
|
|
73
|
+
expect(scheduler.scheduled).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('swallows callback errors by default without logging to console.error', async () => {
|
|
77
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
78
|
+
try {
|
|
79
|
+
const scheduler = new PowerScheduler(() => {
|
|
80
|
+
throw new Error('flush failed');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
scheduler.schedule();
|
|
84
|
+
await Promise.resolve();
|
|
85
|
+
|
|
86
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
87
|
+
expect(scheduler.scheduled).toBe(false);
|
|
88
|
+
} finally {
|
|
89
|
+
errorSpy.mockRestore();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('calls onError when provided and continues scheduling safely', async () => {
|
|
94
|
+
const onError = vi.fn();
|
|
95
|
+
const scheduler = new PowerScheduler(
|
|
96
|
+
() => {
|
|
97
|
+
throw new Error('flush failed');
|
|
98
|
+
},
|
|
99
|
+
{ onError }
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
scheduler.schedule();
|
|
103
|
+
await Promise.resolve();
|
|
104
|
+
|
|
105
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(scheduler.scheduled).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('swallows onError hook failures', async () => {
|
|
110
|
+
const scheduler = new PowerScheduler(
|
|
111
|
+
() => {
|
|
112
|
+
throw new Error('flush failed');
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
onError: () => {
|
|
116
|
+
throw new Error('hook failed');
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
scheduler.schedule();
|
|
122
|
+
await Promise.resolve();
|
|
123
|
+
|
|
124
|
+
expect(scheduler.scheduled).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|