performance-helpers 1.0.0 → 1.0.1
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/README.md +12 -2
- package/package.json +146 -1
- package/src/index.js +1 -0
- package/.eslintrc.cjs +0 -22
- package/.nojekyll +0 -0
- package/.prettierrc +0 -6
- package/CONTRIBUTING.md +0 -178
- package/assets/1_Caching.md +0 -4
- package/assets/2_Parallelizing.md +0 -18
- package/assets/3_Logging.md +0 -3
- package/assets/404.md +0 -3
- package/assets/4_Utils.md +0 -10
- package/assets/logo.png +0 -0
- package/assets/navigation.md +0 -10
- package/bench/README.md +0 -97
- package/bench/results.json +0 -94
- package/bench/results.md +0 -233
- package/bench/run.js +0 -2639
- package/bench/worker.js +0 -43
- package/docs/README.md +0 -38
- package/docs/docs-typedoc.json +0 -38714
- package/docs/helpers/constants/README.md +0 -34
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_BACKOFF_MAX_MULTIPLIER.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_COOLDOWN_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_INTERVAL_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_AUTOSCALE_MIN_INTERVAL_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_BACKPRESSURE_QUEUE_CAPACITY.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_BACKPRESSURE_REFILL_INTERVAL_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_BATCH_MAX_SIZE.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_CACHE_DEFAULT_TTL_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_CACHE_MAX_POOL_SIZE.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_CACHE_MAX_WEIGHT_BYTES.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_HISTOGRAM_BUCKET_COUNT.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_HISTOGRAM_MAX_VALUE.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_MAX_CLEANUP_PER_TICK.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_QUEUE_CAPACITY.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_REAPER_MIN_INTERVAL_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_REFILL_INTERVAL_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_RETRY_BASE_DELAY_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_RETRY_MAX_DELAY_MS.md +0 -9
- package/docs/helpers/constants/variables/DEFAULT_TIMEOUT_MS.md +0 -9
- package/docs/helpers/constants/variables/ENCODE_CACHE_LARGE_KEY_LENGTH.md +0 -9
- package/docs/helpers/constants/variables/MAX_DEEP_EQUAL_DEPTH.md +0 -9
- package/docs/helpers/constants/variables/MS_PER_MIN.md +0 -9
- package/docs/helpers/constants/variables/MS_PER_SEC.md +0 -9
- package/docs/helpers/constants/variables/default.md +0 -103
- package/docs/helpers/jsdoc-types/README.md +0 -33
- package/docs/helpers/jsdoc-types/interfaces/BufferDecoder.md +0 -23
- package/docs/helpers/jsdoc-types/interfaces/BufferEncoder.md +0 -23
- package/docs/helpers/jsdoc-types/interfaces/CacheNode.md +0 -43
- package/docs/helpers/jsdoc-types/interfaces/CommonPoolOptions.md +0 -31
- package/docs/helpers/jsdoc-types/interfaces/PendingResponseEntry.md +0 -51
- package/docs/helpers/jsdoc-types/interfaces/PostMessageOptions.md +0 -39
- package/docs/helpers/jsdoc-types/interfaces/PowerBatchOptions.md +0 -13
- package/docs/helpers/jsdoc-types/interfaces/PowerCacheOptions.md +0 -115
- package/docs/helpers/jsdoc-types/interfaces/PowerChunkingOptions.md +0 -31
- package/docs/helpers/jsdoc-types/interfaces/PowerCircuitOptions.md +0 -45
- package/docs/helpers/jsdoc-types/interfaces/PowerDeadlineOptions.md +0 -101
- package/docs/helpers/jsdoc-types/interfaces/PowerDeferOptions.md +0 -13
- package/docs/helpers/jsdoc-types/interfaces/PowerEventBusOptions.md +0 -19
- package/docs/helpers/jsdoc-types/interfaces/PowerLatchOptions.md +0 -23
- package/docs/helpers/jsdoc-types/interfaces/PowerLoggerOptions.md +0 -51
- package/docs/helpers/jsdoc-types/interfaces/PowerObserverOptions.md +0 -25
- package/docs/helpers/jsdoc-types/interfaces/PowerPoolOptions.md +0 -85
- package/docs/helpers/jsdoc-types/interfaces/PowerQueueOptions.md +0 -13
- package/docs/helpers/jsdoc-types/interfaces/PowerRetryOptions.md +0 -83
- package/docs/helpers/jsdoc-types/interfaces/PowerSlidingWindowOptions.md +0 -19
- package/docs/helpers/jsdoc-types/interfaces/PowerTTLMapOptions.md +0 -27
- package/docs/helpers/jsdoc-types/interfaces/PowerThrottleOptions.md +0 -31
- package/docs/helpers/jsdoc-types/interfaces/WorkerObj.md +0 -55
- package/docs/helpers/powerBackpressure/README.md +0 -17
- package/docs/helpers/powerBackpressure/classes/PowerBackpressure.md +0 -368
- package/docs/helpers/powerBatch/README.md +0 -17
- package/docs/helpers/powerBatch/classes/PowerBatch.md +0 -139
- package/docs/helpers/powerBuffer/README.md +0 -26
- package/docs/helpers/powerBuffer/functions/b2o.md +0 -25
- package/docs/helpers/powerBuffer/functions/o2b.md +0 -23
- package/docs/helpers/powerBuffer/functions/o2u8.md +0 -33
- package/docs/helpers/powerBuffer/functions/u82o.md +0 -30
- package/docs/helpers/powerBulkhead/README.md +0 -17
- package/docs/helpers/powerBulkhead/classes/PowerBulkhead.md +0 -302
- package/docs/helpers/powerCache/README.md +0 -29
- package/docs/helpers/powerCache/classes/PowerCache.md +0 -933
- package/docs/helpers/powerCache/classes/PowerMemoizer.md +0 -244
- package/docs/helpers/powerCache/classes/PowerTimedCache.md +0 -302
- package/docs/helpers/powerCache/functions/simpleArgsKey.md +0 -31
- package/docs/helpers/powerChunking/README.md +0 -17
- package/docs/helpers/powerChunking/classes/PowerChunker.md +0 -78
- package/docs/helpers/powerCircuit/README.md +0 -23
- package/docs/helpers/powerCircuit/classes/PowerCircuit.md +0 -167
- package/docs/helpers/powerDeadline/README.md +0 -23
- package/docs/helpers/powerDeadline/classes/PowerDeadline.md +0 -88
- package/docs/helpers/powerDefer/README.md +0 -17
- package/docs/helpers/powerDefer/classes/PowerDefer.md +0 -134
- package/docs/helpers/powerEventBus/README.md +0 -23
- package/docs/helpers/powerEventBus/classes/PowerEventBus.md +0 -330
- package/docs/helpers/powerHistogram/README.md +0 -17
- package/docs/helpers/powerHistogram/classes/PowerHistogram.md +0 -285
- package/docs/helpers/powerLatch/README.md +0 -17
- package/docs/helpers/powerLatch/classes/PowerLatch.md +0 -264
- package/docs/helpers/powerLogger/README.md +0 -17
- package/docs/helpers/powerLogger/classes/PowerLogger.md +0 -290
- package/docs/helpers/powerObserver/README.md +0 -23
- package/docs/helpers/powerObserver/classes/PowerObserver.md +0 -213
- package/docs/helpers/powerPermitGate/README.md +0 -11
- package/docs/helpers/powerPermitGate/classes/PowerPermitGate.md +0 -248
- package/docs/helpers/powerPool/README.md +0 -36
- package/docs/helpers/powerPool/classes/PowerPool.md +0 -973
- package/docs/helpers/powerPool/classes/PowerPoolShutdownError.md +0 -67
- package/docs/helpers/powerQueue/README.md +0 -11
- package/docs/helpers/powerQueue/classes/PowerQueue.md +0 -302
- package/docs/helpers/powerRateLimit/README.md +0 -17
- package/docs/helpers/powerRateLimit/classes/PowerRateLimit.md +0 -187
- package/docs/helpers/powerRetry/README.md +0 -23
- package/docs/helpers/powerRetry/classes/PowerRetry.md +0 -106
- package/docs/helpers/powerScheduler/README.md +0 -11
- package/docs/helpers/powerScheduler/classes/PowerScheduler.md +0 -135
- package/docs/helpers/powerSemaphore/README.md +0 -17
- package/docs/helpers/powerSemaphore/classes/PowerSemaphore.md +0 -173
- package/docs/helpers/powerSlidingWindow/README.md +0 -11
- package/docs/helpers/powerSlidingWindow/classes/PowerSlidingWindow.md +0 -83
- package/docs/helpers/powerSubscriberSet/README.md +0 -15
- package/docs/helpers/powerSubscriberSet/classes/PowerSubscriberSet.md +0 -251
- package/docs/helpers/powerSubscriberSet/functions/cleanupWeakRefs.md +0 -21
- package/docs/helpers/powerTTLMap/README.md +0 -17
- package/docs/helpers/powerTTLMap/classes/PowerTTLMap.md +0 -326
- package/docs/helpers/powerThrottle/README.md +0 -17
- package/docs/helpers/powerThrottle/classes/PowerThrottle.md +0 -216
- package/docs/index/README.md +0 -205
- package/docs/utils/errors/README.md +0 -12
- package/docs/utils/errors/functions/formatErrorObj.md +0 -30
- package/docs/utils/errors/functions/normalizeError.md +0 -50
- package/docs/utils/now/README.md +0 -19
- package/docs/utils/now/functions/measureAsync.md +0 -37
- package/docs/utils/now/functions/measureSync.md +0 -54
- package/docs/utils/now/functions/nowMs.md +0 -24
- package/guides/autoscale.md +0 -80
- package/guides/errors.md +0 -41
- package/guides/metaGuide.md +0 -440
- package/guides/now.md +0 -56
- package/guides/powerBackpressure.md +0 -110
- package/guides/powerBatch.md +0 -82
- package/guides/powerBuffer.md +0 -86
- package/guides/powerBulkhead.md +0 -61
- package/guides/powerCache.md +0 -269
- package/guides/powerChunking.md +0 -130
- package/guides/powerCircuit.md +0 -84
- package/guides/powerDeadline.md +0 -99
- package/guides/powerDefer.md +0 -56
- package/guides/powerEventBus.md +0 -89
- package/guides/powerHistogram.md +0 -71
- package/guides/powerLatch.md +0 -94
- package/guides/powerLogger.md +0 -129
- package/guides/powerObserver.md +0 -65
- package/guides/powerPermitGate.md +0 -52
- package/guides/powerPool.md +0 -321
- package/guides/powerQueue.md +0 -112
- package/guides/powerRateLimit.md +0 -37
- package/guides/powerRetry.md +0 -54
- package/guides/powerScheduler.md +0 -41
- package/guides/powerSemaphore.md +0 -65
- package/guides/powerSlidingWindow.md +0 -63
- package/guides/powerSubscriberSet.md +0 -48
- package/guides/powerTTLMap.md +0 -58
- package/guides/powerThrottle.md +0 -152
- package/index.html +0 -57
- package/results.json +0 -6692
- package/scripts/find-missing-jsdoc.js +0 -62
- package/scripts/modernize-optional-chaining.cjs +0 -36
- package/scripts/pool-debug.mjs +0 -29
- package/scripts/repro_powercache.js +0 -14
- package/scripts/static-audit-exports.cjs +0 -93
- package/scripts/static-audit-exports.json +0 -518
- package/test/powerBackpressure.test.js +0 -114
- package/test/powerBatch.branches.extra.test.js +0 -122
- package/test/powerBatch.test.js +0 -79
- package/test/powerBuffer.test.js +0 -125
- package/test/powerBulkhead.test.js +0 -210
- package/test/powerCache.branches.test.js +0 -233
- package/test/powerCache.bulk.test.js +0 -31
- package/test/powerCache.getorset.test.js +0 -110
- package/test/powerCache.hitRate.test.js +0 -35
- package/test/powerCache.inflight.test.js +0 -24
- package/test/powerCache.iterator.test.js +0 -18
- package/test/powerCache.misses.test.js +0 -52
- package/test/powerCache.more.test.js +0 -118
- package/test/powerCache.test.js +0 -37
- package/test/powerCache.timeout.test.js +0 -25
- package/test/powerCache.touch.test.js +0 -46
- package/test/powerChunking.branches.extra.test.js +0 -155
- package/test/powerChunking.errors.test.js +0 -177
- package/test/powerChunking.test.js +0 -39
- package/test/powerCircuit.observability.test.js +0 -71
- package/test/powerCircuit.test.js +0 -74
- package/test/powerDeadline.test.js +0 -140
- package/test/powerDefer.test.js +0 -55
- package/test/powerErrors.test.js +0 -32
- package/test/powerEventBus.branches.extra.test.js +0 -70
- package/test/powerEventBus.extra.test.js +0 -72
- package/test/powerEventBus.max.test.js +0 -43
- package/test/powerEventBus.more.test.js +0 -121
- package/test/powerEventBus.once_off.test.js +0 -17
- package/test/powerEventBus.test.js +0 -74
- package/test/powerEventBus.uncovered.test.js +0 -57
- package/test/powerEventBus.weak.test.js +0 -18
- package/test/powerHistogram.test.js +0 -73
- package/test/powerLatch.branches.extra.test.js +0 -115
- package/test/powerLatch.test.js +0 -57
- package/test/powerLogger.branches.test.js +0 -98
- package/test/powerLogger.formatter.name.test.js +0 -58
- package/test/powerLogger.json.test.js +0 -88
- package/test/powerLogger.output.test.js +0 -81
- package/test/powerLogger.table.debug.test.js +0 -77
- package/test/powerLogger.test.js +0 -59
- package/test/powerMemoizer.memoize.test.js +0 -100
- package/test/powerMemoizer.test.js +0 -85
- package/test/powerObserver.test.js +0 -129
- package/test/powerPermitGate.test.js +0 -66
- package/test/powerPool.autoTransfer.test.js +0 -100
- package/test/powerPool.autoscale.extra.test.js +0 -88
- package/test/powerPool.autoscale.test.js +0 -136
- package/test/powerPool.awaitDefaultTimeout.test.js +0 -52
- package/test/powerPool.awaitTimeout.test.js +0 -22
- package/test/powerPool.batch.test.js +0 -170
- package/test/powerPool.branches.extra2.test.js +0 -42
- package/test/powerPool.branches.test.js +0 -102
- package/test/powerPool.browser.messageerror.test.js +0 -45
- package/test/powerPool.correlation.test.js +0 -26
- package/test/powerPool.correlationId.test.js +0 -63
- package/test/powerPool.dispose.test.js +0 -49
- package/test/powerPool.drain.test.js +0 -57
- package/test/powerPool.events.test.js +0 -131
- package/test/powerPool.more.extra.test.js +0 -99
- package/test/powerPool.more.test.js +0 -283
- package/test/powerPool.node.messageerror.test.js +0 -46
- package/test/powerPool.postMessage.promise.test.js +0 -83
- package/test/powerPool.queueHigh.test.js +0 -55
- package/test/powerPool.queueSaturation.test.js +0 -51
- package/test/powerPool.rapidResize.test.js +0 -55
- package/test/powerPool.resize.overload.test.js +0 -65
- package/test/powerPool.resize.test.js +0 -70
- package/test/powerPool.shutdown.test.js +0 -38
- package/test/powerPool.stats.test.js +0 -40
- package/test/powerPool.stopThePress.test.js +0 -94
- package/test/powerPool.terminateShutdown.test.js +0 -22
- package/test/powerPool.test.js +0 -525
- package/test/powerPool.timers.test.js +0 -55
- package/test/powerPool.uncovered.test.js +0 -407
- package/test/powerPool.workerId.test.js +0 -47
- package/test/powerQueue.iterators.test.js +0 -67
- package/test/powerQueue.saturation.test.js +0 -18
- package/test/powerQueue.test.js +0 -48
- package/test/powerQueue.unshiftMany.test.js +0 -49
- package/test/powerRateLimit.atomic.test.js +0 -80
- package/test/powerRateLimit.extra.test.js +0 -145
- package/test/powerRateLimit.functions.test.js +0 -106
- package/test/powerRateLimit.test.js +0 -38
- package/test/powerRetry.attemptTimeout.test.js +0 -51
- package/test/powerRetry.test.js +0 -121
- package/test/powerScheduler.test.js +0 -126
- package/test/powerSemaphore.test.js +0 -108
- package/test/powerSlidingWindow.pool.test.js +0 -55
- package/test/powerSlidingWindow.test.js +0 -25
- package/test/powerSubscriberSet.test.js +0 -125
- package/test/powerTTLMap.test.js +0 -125
- package/test/powerThrottle.pool.test.js +0 -54
- package/test/powerThrottle.refill.test.js +0 -22
- package/test/powerThrottle.reserve.test.js +0 -46
- package/test/powerThrottle.test.js +0 -45
- package/test/powerTimedCache.test.js +0 -73
- package/test/umd.bundle.branches.test.js +0 -100
- package/test/umd.bundle.cache-timers.test.js +0 -48
- package/test/umd.bundle.exhaustive.test.js +0 -158
- package/test/umd.bundle.fuzz.test.js +0 -86
- package/test/umd.bundle.hasEqual.more.test.js +0 -68
- package/test/umd.bundle.hasEqual.test.js +0 -104
- package/test/umd.bundle.logger-extra.test.js +0 -48
- package/test/umd.bundle.more-coverage-2.test.js +0 -67
- package/test/umd.bundle.pool.test.js +0 -134
- package/test/umd.bundle.test.js +0 -265
- package/test/utils.measure.test.js +0 -49
- package/test/utils.now.extra.test.js +0 -30
- package/test/utils.now.more.test.js +0 -57
- package/tsconfig.json +0 -16
- package/typedoc.json +0 -25
- package/vite.config.js +0 -31
- package/vitest.config.js +0 -17
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerCache } from '../src/helpers/powerCache.js';
|
|
3
|
-
|
|
4
|
-
function delay(ms) {
|
|
5
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
describe('PowerCache.touch()', () => {
|
|
9
|
-
it('updates recency without changing value', () => {
|
|
10
|
-
const c = new PowerCache();
|
|
11
|
-
c.set('a', 1);
|
|
12
|
-
c.set('b', 2);
|
|
13
|
-
// MRU order should be b, a
|
|
14
|
-
const before = Array.from(c.entries('MRU')).map(([k]) => k);
|
|
15
|
-
expect(before).toEqual(['b', 'a']);
|
|
16
|
-
|
|
17
|
-
const ok = c.touch('a');
|
|
18
|
-
expect(ok).toBe(true);
|
|
19
|
-
const after = Array.from(c.entries('MRU')).map(([k]) => k);
|
|
20
|
-
expect(after).toEqual(['a', 'b']);
|
|
21
|
-
// value unchanged
|
|
22
|
-
expect(c.get('a')).toBe(1);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('returns false and removes expired entries', async () => {
|
|
26
|
-
const c = new PowerCache();
|
|
27
|
-
// set with very short TTL
|
|
28
|
-
c.set('x', 42, { ttl: 5 });
|
|
29
|
-
await delay(20);
|
|
30
|
-
// now expired
|
|
31
|
-
const ok = c.touch('x');
|
|
32
|
-
expect(ok).toBe(false);
|
|
33
|
-
expect(c.get('x')).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('refreshes TTL when provided', async () => {
|
|
37
|
-
const c = new PowerCache();
|
|
38
|
-
c.set('k', 'v', { ttl: 10 });
|
|
39
|
-
// touch with longer ttl
|
|
40
|
-
const ok = c.touch('k', 1000);
|
|
41
|
-
expect(ok).toBe(true);
|
|
42
|
-
// wait for original TTL to have passed but before refreshed TTL
|
|
43
|
-
await delay(50);
|
|
44
|
-
expect(c.get('k')).toBe('v');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerChunker } from '../src/helpers/powerChunking.js';
|
|
3
|
-
import { PowerPool } from '../src/helpers/powerPool.js';
|
|
4
|
-
import { o2u8 } from '../src/helpers/powerBuffer.js';
|
|
5
|
-
|
|
6
|
-
describe('PowerChunker branches extra', () => {
|
|
7
|
-
it('throws when missing iterable or fn not function', () => {
|
|
8
|
-
expect(() => new PowerChunker(null, () => {})).toThrow();
|
|
9
|
-
expect(() => new PowerChunker([1, 2, 3], null)).toThrow();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('processes array and emits error objects for thrown items', async () => {
|
|
13
|
-
const items = [1, 2, 3];
|
|
14
|
-
const messages = [];
|
|
15
|
-
const pool = new PowerChunker(
|
|
16
|
-
items,
|
|
17
|
-
(item) => {
|
|
18
|
-
if (item === 2) throw new Error('boom');
|
|
19
|
-
return item * 2;
|
|
20
|
-
},
|
|
21
|
-
{ chunkSize: 2 }
|
|
22
|
-
);
|
|
23
|
-
pool.onmessage = (e) => messages.push(e.data);
|
|
24
|
-
await pool.drain();
|
|
25
|
-
// At least one message should contain an error result for the thrown item
|
|
26
|
-
const allResults = messages.flatMap((m) => m.results || []);
|
|
27
|
-
expect(allResults.some((r) => r && r.error)).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('streams an iterator and returns the pool instance when iterable throws', async () => {
|
|
31
|
-
function* badGen() {
|
|
32
|
-
yield 1;
|
|
33
|
-
yield 2;
|
|
34
|
-
throw new Error('stream fail');
|
|
35
|
-
}
|
|
36
|
-
const pool = new PowerChunker(badGen(), (n) => n + 1);
|
|
37
|
-
// ensure we get a pool back and drain resolves without throwing
|
|
38
|
-
expect(pool).toBeTruthy();
|
|
39
|
-
await pool.drain();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('handles async item functions and preserves correlationId on direct pool posts', async () => {
|
|
43
|
-
const pool = new PowerChunker([], async (item) => item * 2, { chunkSize: 2 });
|
|
44
|
-
|
|
45
|
-
const payload = await new Promise((resolve) => {
|
|
46
|
-
pool.onmessage = (e) => resolve(e.data);
|
|
47
|
-
pool.postMessage({ chunk: [2, 4], correlationId: 'cid-1' });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
expect(payload.processed).toBe(2);
|
|
51
|
-
expect(payload.results).toEqual([4, 8]);
|
|
52
|
-
expect(payload.correlationId).toBe('cid-1');
|
|
53
|
-
pool.terminate();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('continues streaming when pool.postMessage throws for a chunk', async () => {
|
|
57
|
-
const original = PowerPool.prototype.postMessage;
|
|
58
|
-
let calls = 0;
|
|
59
|
-
|
|
60
|
-
PowerPool.prototype.postMessage = function patchedPostMessage(...args) {
|
|
61
|
-
calls += 1;
|
|
62
|
-
if (calls === 1) throw new Error('chunk dispatch failed');
|
|
63
|
-
return original.apply(this, args);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const values = function* () {
|
|
68
|
-
yield 1;
|
|
69
|
-
yield 2;
|
|
70
|
-
yield 3;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const seen = [];
|
|
74
|
-
const pool = new PowerChunker(values(), (value) => value + 1, { chunkSize: 1 });
|
|
75
|
-
pool.onmessage = (e) => {
|
|
76
|
-
seen.push(...(e.data.results || []));
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
await pool.drain();
|
|
80
|
-
expect(seen.length).toBeGreaterThan(0);
|
|
81
|
-
expect(seen).toContain(3);
|
|
82
|
-
expect(seen).toContain(4);
|
|
83
|
-
pool.terminate();
|
|
84
|
-
} finally {
|
|
85
|
-
PowerPool.prototype.postMessage = original;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('decodes transferable chunk payloads and supports inline worker add/remove listeners', async () => {
|
|
90
|
-
const pool = new PowerChunker([], (value) => value + 1, {
|
|
91
|
-
poolOptions: { size: 1, minSize: 1, lazy: false },
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const worker = pool.workers[0].worker._underlying;
|
|
96
|
-
const seen = [];
|
|
97
|
-
const onMessage = (e) => seen.push(e.data);
|
|
98
|
-
worker.addEventListener('message', onMessage);
|
|
99
|
-
|
|
100
|
-
worker.postMessage(o2u8({ chunk: [2], correlationId: 'buf-1' }));
|
|
101
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
102
|
-
|
|
103
|
-
expect(seen[0]).toMatchObject({
|
|
104
|
-
processed: 1,
|
|
105
|
-
results: [3],
|
|
106
|
-
correlationId: 'buf-1',
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
worker.removeEventListener('message', onMessage);
|
|
110
|
-
worker.postMessage(o2u8({ chunk: [5] }));
|
|
111
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
112
|
-
|
|
113
|
-
expect(seen).toHaveLength(1);
|
|
114
|
-
} finally {
|
|
115
|
-
pool.terminate();
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('routes inline worker failures to onerror and stops processing after terminate()', async () => {
|
|
120
|
-
const pool = new PowerChunker([], (value) => value + 1, {
|
|
121
|
-
poolOptions: { size: 1, minSize: 1, lazy: false },
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const worker = pool.workers[0].worker._underlying;
|
|
126
|
-
const errors = [];
|
|
127
|
-
const messages = [];
|
|
128
|
-
const onError = (err) => errors.push(err);
|
|
129
|
-
worker.addEventListener('error', onError);
|
|
130
|
-
worker.onmessage = () => {
|
|
131
|
-
throw new Error('message handler failed');
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
worker.postMessage({ chunk: [1] });
|
|
135
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
136
|
-
expect(errors.some((err) => /message handler failed/.test(String(err && err.message)))).toBe(
|
|
137
|
-
true
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
worker.onmessage = (e) => messages.push(e.data);
|
|
141
|
-
worker.postMessage(o2u8(null));
|
|
142
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
143
|
-
expect(errors.length).toBeGreaterThanOrEqual(2);
|
|
144
|
-
|
|
145
|
-
worker.terminate();
|
|
146
|
-
worker.postMessage({ chunk: [9] });
|
|
147
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
148
|
-
expect(messages).toHaveLength(0);
|
|
149
|
-
|
|
150
|
-
worker.removeEventListener('error', onError);
|
|
151
|
-
} finally {
|
|
152
|
-
pool.terminate();
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
});
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import { describe, it } from 'vitest';
|
|
3
|
-
import { PowerChunker } from '../src/helpers/powerChunking.js';
|
|
4
|
-
|
|
5
|
-
describe('PowerChunking error handling', () => {
|
|
6
|
-
it('emits per-item normalized error objects for sync throws and async rejections', async () => {
|
|
7
|
-
const items = [1, 2, 3, 4];
|
|
8
|
-
// 2 throws sync with code, 3 rejects async with code
|
|
9
|
-
const fn = (item) => {
|
|
10
|
-
if (item === 2) {
|
|
11
|
-
const e = new Error('sync fail');
|
|
12
|
-
e.code = 'SYNC_CODE';
|
|
13
|
-
throw e;
|
|
14
|
-
}
|
|
15
|
-
if (item === 3) {
|
|
16
|
-
const e = new Error('async fail');
|
|
17
|
-
e.code = 'ASYNC_CODE';
|
|
18
|
-
return Promise.reject(e);
|
|
19
|
-
}
|
|
20
|
-
return item * 10;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const pool = new PowerChunker(items, fn, { poolOptions: { size: 2 }, chunkSize: 2 });
|
|
24
|
-
|
|
25
|
-
const chunkResults = [];
|
|
26
|
-
pool.onmessage = (e) => {
|
|
27
|
-
if (e && e.data) chunkResults.push(e.data);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
await pool.drain();
|
|
31
|
-
|
|
32
|
-
// flatten per-item results
|
|
33
|
-
const all = chunkResults.flatMap((c) => c.results || []);
|
|
34
|
-
|
|
35
|
-
// should contain two errors (SYNC_CODE and ASYNC_CODE) and two numeric results
|
|
36
|
-
const codes = all
|
|
37
|
-
.filter((r) => r && r.error)
|
|
38
|
-
.map((r) => r.code)
|
|
39
|
-
.sort();
|
|
40
|
-
expect(codes).to.include('SYNC_CODE');
|
|
41
|
-
expect(codes).to.include('ASYNC_CODE');
|
|
42
|
-
|
|
43
|
-
const numbers = all.filter((r) => !r || !r.error);
|
|
44
|
-
// two successful items 1 and 4 -> 10 and 40
|
|
45
|
-
expect(numbers).to.include(10);
|
|
46
|
-
expect(numbers).to.include(40);
|
|
47
|
-
|
|
48
|
-
pool.terminate();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('surfaces batch dispatch failures via pool error listeners', async () => {
|
|
52
|
-
const pool = new PowerChunker([1, 2, 3], (item) => item, {
|
|
53
|
-
chunkSize: 1,
|
|
54
|
-
poolOptions: {
|
|
55
|
-
size: 1,
|
|
56
|
-
minSize: 1,
|
|
57
|
-
maxSize: 1,
|
|
58
|
-
maxTasksPerWorker: 0,
|
|
59
|
-
taskQueue: true,
|
|
60
|
-
queuePolicy: 'reject',
|
|
61
|
-
lazy: false,
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const errors = [];
|
|
66
|
-
pool.addEventListener('error', (err) => errors.push(err));
|
|
67
|
-
|
|
68
|
-
await Promise.resolve();
|
|
69
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
70
|
-
|
|
71
|
-
expect(errors.length).to.be.greaterThan(0);
|
|
72
|
-
expect(errors[0]).to.include({ code: 'ECHUNKDISPATCH', mode: 'batch' });
|
|
73
|
-
|
|
74
|
-
pool.terminate();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('reports global failed chunk indexes across array dispatch windows', async () => {
|
|
78
|
-
const items = Array.from({ length: 20 }, (_, i) => i + 1);
|
|
79
|
-
const pool = new PowerChunker(items, (item) => item, {
|
|
80
|
-
chunkSize: 1,
|
|
81
|
-
poolOptions: {
|
|
82
|
-
size: 1,
|
|
83
|
-
minSize: 1,
|
|
84
|
-
maxSize: 1,
|
|
85
|
-
maxTasksPerWorker: 0,
|
|
86
|
-
taskQueue: true,
|
|
87
|
-
queuePolicy: 'reject',
|
|
88
|
-
lazy: false,
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const errors = [];
|
|
93
|
-
pool.addEventListener('error', (err) => errors.push(err));
|
|
94
|
-
|
|
95
|
-
await Promise.resolve();
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
97
|
-
|
|
98
|
-
const batchErrors = errors.filter(
|
|
99
|
-
(err) => err && err.code === 'ECHUNKDISPATCH' && err.mode === 'batch'
|
|
100
|
-
);
|
|
101
|
-
expect(batchErrors.length).to.be.greaterThan(0);
|
|
102
|
-
for (const err of batchErrors) {
|
|
103
|
-
expect(Array.isArray(err.failedChunks)).to.equal(true);
|
|
104
|
-
for (const idx of err.failedChunks) {
|
|
105
|
-
expect(Number.isInteger(idx)).to.equal(true);
|
|
106
|
-
expect(idx).to.be.at.least(0);
|
|
107
|
-
expect(idx).to.be.below(items.length);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
pool.terminate();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('surfaces stream dispatch failures via pool error listeners', async () => {
|
|
115
|
-
function* items() {
|
|
116
|
-
yield 1;
|
|
117
|
-
yield 2;
|
|
118
|
-
yield 3;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const pool = new PowerChunker(items(), (item) => item, {
|
|
122
|
-
chunkSize: 1,
|
|
123
|
-
poolOptions: {
|
|
124
|
-
size: 1,
|
|
125
|
-
minSize: 1,
|
|
126
|
-
maxSize: 1,
|
|
127
|
-
maxTasksPerWorker: 0,
|
|
128
|
-
taskQueue: true,
|
|
129
|
-
queuePolicy: 'reject',
|
|
130
|
-
lazy: false,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const errors = [];
|
|
135
|
-
pool.addEventListener('error', (err) => errors.push(err));
|
|
136
|
-
|
|
137
|
-
await Promise.resolve();
|
|
138
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
139
|
-
|
|
140
|
-
expect(
|
|
141
|
-
errors.some((err) => err && err.code === 'ECHUNKDISPATCH' && err.mode === 'stream')
|
|
142
|
-
).to.equal(true);
|
|
143
|
-
|
|
144
|
-
pool.terminate();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('emits stream-iterate errors when the input iterable throws', async () => {
|
|
148
|
-
function* badItems() {
|
|
149
|
-
yield 1;
|
|
150
|
-
throw new Error('iter boom');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const pool = new PowerChunker(badItems(), (item) => item, {
|
|
154
|
-
chunkSize: 1,
|
|
155
|
-
poolOptions: {
|
|
156
|
-
size: 1,
|
|
157
|
-
minSize: 1,
|
|
158
|
-
maxSize: 1,
|
|
159
|
-
taskQueue: true,
|
|
160
|
-
queuePolicy: 'enqueue',
|
|
161
|
-
lazy: false,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const errors = [];
|
|
166
|
-
pool.addEventListener('error', (err) => errors.push(err));
|
|
167
|
-
|
|
168
|
-
await Promise.resolve();
|
|
169
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
170
|
-
|
|
171
|
-
expect(
|
|
172
|
-
errors.some((err) => err && err.code === 'ECHUNKDISPATCH' && err.mode === 'stream-iterate')
|
|
173
|
-
).to.equal(true);
|
|
174
|
-
|
|
175
|
-
pool.terminate();
|
|
176
|
-
});
|
|
177
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import { describe, it } from 'vitest';
|
|
3
|
-
import { PowerChunker } from '../src/helpers/powerChunking.js';
|
|
4
|
-
|
|
5
|
-
describe('PowerChunking helper', () => {
|
|
6
|
-
it('processes all items in chunks and returns processed counts (awaitResponse)', async () => {
|
|
7
|
-
const items = Array.from({ length: 50 }, (_, i) => i + 1);
|
|
8
|
-
const processed = [];
|
|
9
|
-
const fn = (item) => {
|
|
10
|
-
processed.push(item);
|
|
11
|
-
};
|
|
12
|
-
const pool = new PowerChunker(items, fn, { poolOptions: { size: 2 } });
|
|
13
|
-
expect(pool && typeof pool.postMessageBatch === 'function').to.equal(true);
|
|
14
|
-
// wait until the pool has finished processing all chunks
|
|
15
|
-
await pool.drain();
|
|
16
|
-
// ensure the fn ran for each item
|
|
17
|
-
expect(processed.length).to.equal(items.length);
|
|
18
|
-
pool.terminate();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('uses explicit chunkSize when provided', () => {
|
|
22
|
-
const items = Array.from({ length: 10 }, (_, i) => i);
|
|
23
|
-
const fn = () => {};
|
|
24
|
-
const pool = new PowerChunker(items, fn, { poolOptions: { size: 2 }, chunkSize: 3 });
|
|
25
|
-
// Each processed chunk emits one `message` event; wait for drain and verify
|
|
26
|
-
let messageCount = 0;
|
|
27
|
-
let totalProcessed = 0;
|
|
28
|
-
pool.onmessage = (e) => {
|
|
29
|
-
messageCount++;
|
|
30
|
-
if (e && e.data && e.data.processed) totalProcessed += e.data.processed;
|
|
31
|
-
};
|
|
32
|
-
return pool.drain().then(() => {
|
|
33
|
-
// Ensure all items were processed and at least the expected number of chunk messages occurred
|
|
34
|
-
expect(totalProcessed).to.equal(items.length);
|
|
35
|
-
expect(messageCount).to.be.at.least(Math.ceil(items.length / 3));
|
|
36
|
-
pool.terminate();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import PowerCircuit from '../src/helpers/powerCircuit.js';
|
|
3
|
-
import { PowerEventBus } from '../src/helpers/powerEventBus.js';
|
|
4
|
-
|
|
5
|
-
describe('PowerCircuit observability', () => {
|
|
6
|
-
it('calls onStateChange callback when state transitions occur', async () => {
|
|
7
|
-
const calls = [];
|
|
8
|
-
const cb = new PowerCircuit({
|
|
9
|
-
threshold: 1,
|
|
10
|
-
timeout: 10,
|
|
11
|
-
onStateChange: (s, r) => calls.push([s, r]),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
// cause a failure to open the circuit
|
|
15
|
-
await cb.call(() => Promise.reject(new Error('fail'))).catch(() => {});
|
|
16
|
-
|
|
17
|
-
// after one failure threshold=1 -> should open
|
|
18
|
-
expect(calls.length).toBeGreaterThanOrEqual(1);
|
|
19
|
-
expect(calls[0][0]).toBe('open');
|
|
20
|
-
expect(calls[0][1]).toBe('thresholdExceeded');
|
|
21
|
-
|
|
22
|
-
// advance through timeout by waiting
|
|
23
|
-
await new Promise((res) => setTimeout(res, 15));
|
|
24
|
-
|
|
25
|
-
// next call should cause half-open then success -> closed
|
|
26
|
-
await cb.call(() => Promise.resolve('ok'));
|
|
27
|
-
|
|
28
|
-
// find closed event
|
|
29
|
-
const closed = calls.find(([s]) => s === 'closed');
|
|
30
|
-
expect(closed).toBeTruthy();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('swallows errors thrown by onStateChange and event bus observers', async () => {
|
|
34
|
-
const bus = new PowerEventBus();
|
|
35
|
-
bus.on('stateChange', () => {
|
|
36
|
-
throw new Error('bus observer failed');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const cb = new PowerCircuit({
|
|
40
|
-
threshold: 1,
|
|
41
|
-
timeout: 10,
|
|
42
|
-
eventBus: bus,
|
|
43
|
-
onStateChange() {
|
|
44
|
-
throw new Error('callback failed');
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
await expect(cb.call(() => Promise.reject(new Error('fail')))).rejects.toThrow('fail');
|
|
49
|
-
expect(cb.state).toBe('open');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('emits stateChange on provided PowerEventBus', async () => {
|
|
53
|
-
const bus = new PowerEventBus();
|
|
54
|
-
const events = [];
|
|
55
|
-
bus.on('stateChange', (payload) => events.push(payload));
|
|
56
|
-
|
|
57
|
-
const cb = new PowerCircuit({ threshold: 1, timeout: 10, eventBus: bus });
|
|
58
|
-
|
|
59
|
-
await cb.call(() => Promise.reject(new Error('boom'))).catch(() => {});
|
|
60
|
-
|
|
61
|
-
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
62
|
-
expect(events[0].state).toBe('open');
|
|
63
|
-
expect(events[0].reason).toBe('thresholdExceeded');
|
|
64
|
-
|
|
65
|
-
await new Promise((res) => setTimeout(res, 15));
|
|
66
|
-
await cb.call(() => Promise.resolve('ok'));
|
|
67
|
-
|
|
68
|
-
const closedEvent = events.find((e) => e.state === 'closed');
|
|
69
|
-
expect(closedEvent).toBeTruthy();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerCircuit } from '../src/helpers/powerCircuit.js';
|
|
3
|
-
|
|
4
|
-
describe('PowerCircuit', () => {
|
|
5
|
-
it('throws when fn is not a function', async () => {
|
|
6
|
-
const cb = new PowerCircuit();
|
|
7
|
-
await expect(cb.call(null)).rejects.toThrow('fn must be a function');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('opens after threshold failures and short-circuits', async () => {
|
|
11
|
-
const cb = new PowerCircuit({ threshold: 2, timeout: 50 });
|
|
12
|
-
let fail = true;
|
|
13
|
-
const f = async () => {
|
|
14
|
-
if (fail) throw new Error('boom');
|
|
15
|
-
return 'ok';
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
await expect(cb.call(f)).rejects.toThrow('boom');
|
|
19
|
-
expect(cb.failures).toBe(1);
|
|
20
|
-
// second failure triggers open
|
|
21
|
-
await expect(cb.call(f)).rejects.toThrow('boom');
|
|
22
|
-
expect(cb.state).toBe('open');
|
|
23
|
-
expect(cb.failures).toBe(0);
|
|
24
|
-
expect(cb.lastError).toBeInstanceOf(Error);
|
|
25
|
-
// subsequent calls short-circuit
|
|
26
|
-
await expect(cb.call(f)).rejects.toHaveProperty('code', 'ECIRCUITOPEN');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('allows trial after timeout and recovers on success', async () => {
|
|
30
|
-
const cb = new PowerCircuit({ threshold: 1, timeout: 50 });
|
|
31
|
-
// cause one failure to open
|
|
32
|
-
await expect(cb.call(() => Promise.reject(new Error('e1')))).rejects.toThrow();
|
|
33
|
-
expect(cb.state).toBe('open');
|
|
34
|
-
// wait for timeout to allow half-open
|
|
35
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
36
|
-
expect(cb.state).toBe('half-open');
|
|
37
|
-
// now succeed
|
|
38
|
-
const res = await cb.call(() => Promise.resolve('ok'));
|
|
39
|
-
expect(res).toBe('ok');
|
|
40
|
-
expect(cb.state).toBe('closed');
|
|
41
|
-
expect(cb.failures).toBe(0);
|
|
42
|
-
expect(cb.lastError).toBe(null);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('reopens when the half-open trial fails', async () => {
|
|
46
|
-
const cb = new PowerCircuit({ threshold: 1, timeout: 10 });
|
|
47
|
-
|
|
48
|
-
await expect(cb.call(() => Promise.reject(new Error('first fail')))).rejects.toThrow(
|
|
49
|
-
'first fail'
|
|
50
|
-
);
|
|
51
|
-
expect(cb.state).toBe('open');
|
|
52
|
-
|
|
53
|
-
await new Promise((r) => setTimeout(r, 15));
|
|
54
|
-
expect(cb.state).toBe('half-open');
|
|
55
|
-
|
|
56
|
-
await expect(cb.call(() => Promise.reject(new Error('trial fail')))).rejects.toThrow(
|
|
57
|
-
'trial fail'
|
|
58
|
-
);
|
|
59
|
-
expect(cb.state).toBe('open');
|
|
60
|
-
expect(cb.failures).toBe(0);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('reset closes the circuit and clears failure state', async () => {
|
|
64
|
-
const cb = new PowerCircuit({ threshold: 1, timeout: 50 });
|
|
65
|
-
await expect(cb.call(() => Promise.reject(new Error('boom')))).rejects.toThrow('boom');
|
|
66
|
-
expect(cb.state).toBe('open');
|
|
67
|
-
|
|
68
|
-
cb.reset();
|
|
69
|
-
|
|
70
|
-
expect(cb.state).toBe('closed');
|
|
71
|
-
expect(cb.failures).toBe(0);
|
|
72
|
-
expect(cb.lastError).toBe(null);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerDeadline } from '../src/helpers/powerDeadline.js';
|
|
3
|
-
|
|
4
|
-
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
-
|
|
6
|
-
describe('PowerDeadline', () => {
|
|
7
|
-
it('resolves when the function completes before timeout', async () => {
|
|
8
|
-
const value = await PowerDeadline.run(
|
|
9
|
-
async () => {
|
|
10
|
-
await delay(10);
|
|
11
|
-
return 'ok';
|
|
12
|
-
},
|
|
13
|
-
{ attemptTimeout: 100 }
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
expect(value).toBe('ok');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('rejects with ETIMEOUT when an attempt exceeds attemptTimeout', async () => {
|
|
20
|
-
await expect(
|
|
21
|
-
PowerDeadline.run(
|
|
22
|
-
async () => {
|
|
23
|
-
await delay(50);
|
|
24
|
-
},
|
|
25
|
-
{ attemptTimeout: 10 }
|
|
26
|
-
)
|
|
27
|
-
).rejects.toHaveProperty('code', 'ETIMEOUT');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('rejects with EDEADLINE when totalTimeout is exceeded', async () => {
|
|
31
|
-
await expect(
|
|
32
|
-
PowerDeadline.run(
|
|
33
|
-
async () => {
|
|
34
|
-
await delay(100);
|
|
35
|
-
},
|
|
36
|
-
{ maxAttempts: 3, totalTimeout: 50, retryDelay: 10 }
|
|
37
|
-
)
|
|
38
|
-
).rejects.toHaveProperty('code', 'EDEADLINE');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('retries failed attempts when retryIf returns true', async () => {
|
|
42
|
-
let calls = 0;
|
|
43
|
-
const value = await PowerDeadline.run(
|
|
44
|
-
async () => {
|
|
45
|
-
calls += 1;
|
|
46
|
-
if (calls < 2) throw new Error('try again');
|
|
47
|
-
return 'done';
|
|
48
|
-
},
|
|
49
|
-
{ maxAttempts: 3, retryDelay: 5, retryIf: () => true }
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
expect(value).toBe('done');
|
|
53
|
-
expect(calls).toBe(2);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('supports AbortSignal cancellation', async () => {
|
|
57
|
-
const controller = new AbortController();
|
|
58
|
-
const promise = PowerDeadline.run(
|
|
59
|
-
async () => {
|
|
60
|
-
await delay(50);
|
|
61
|
-
},
|
|
62
|
-
{ signal: controller.signal, attemptTimeout: 100 }
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
controller.abort();
|
|
66
|
-
await expect(promise).rejects.toHaveProperty('code', 'EABORT');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('propagates attempts metadata on rejection', async () => {
|
|
70
|
-
await expect(
|
|
71
|
-
PowerDeadline.run(
|
|
72
|
-
async () => {
|
|
73
|
-
throw new Error('bad');
|
|
74
|
-
},
|
|
75
|
-
{ maxAttempts: 2, retryIf: () => false }
|
|
76
|
-
)
|
|
77
|
-
).rejects.toMatchObject({ attempts: 1, attemptTimeout: null });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('throws TypeError when fn is not callable', async () => {
|
|
81
|
-
await expect(PowerDeadline.run(null)).rejects.toThrow(TypeError);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('rejects immediately when the abort signal is already aborted', async () => {
|
|
85
|
-
const controller = new AbortController();
|
|
86
|
-
controller.abort('user cancelled');
|
|
87
|
-
|
|
88
|
-
await expect(
|
|
89
|
-
PowerDeadline.run(async () => 'never', { signal: controller.signal, totalTimeout: 25 })
|
|
90
|
-
).rejects.toMatchObject({
|
|
91
|
-
code: 'EABORT',
|
|
92
|
-
reason: 'user cancelled',
|
|
93
|
-
attempts: 1,
|
|
94
|
-
totalTimeout: 25,
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('treats boolean retryIf values as retry predicates', async () => {
|
|
99
|
-
let calls = 0;
|
|
100
|
-
const value = await PowerDeadline.run(
|
|
101
|
-
async () => {
|
|
102
|
-
calls += 1;
|
|
103
|
-
if (calls === 1) throw new Error('retry once');
|
|
104
|
-
return 'ok';
|
|
105
|
-
},
|
|
106
|
-
{ maxAttempts: 2, retryIf: true }
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
expect(value).toBe('ok');
|
|
110
|
-
expect(calls).toBe(2);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('merges instance defaults with per-call overrides', async () => {
|
|
114
|
-
let calls = 0;
|
|
115
|
-
const deadline = new PowerDeadline({ maxAttempts: 3, retryIf: true });
|
|
116
|
-
const value = await deadline.run(
|
|
117
|
-
async () => {
|
|
118
|
-
calls += 1;
|
|
119
|
-
if (calls < 2) throw new Error('retry from instance');
|
|
120
|
-
return 'done';
|
|
121
|
-
},
|
|
122
|
-
{ retryDelay: 1 }
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
expect(value).toBe('done');
|
|
126
|
-
expect(calls).toBe(2);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('stops before an attempt starts when the total deadline is already exhausted', async () => {
|
|
130
|
-
await expect(
|
|
131
|
-
PowerDeadline.run(
|
|
132
|
-
async () => {
|
|
133
|
-
await delay(10);
|
|
134
|
-
throw new Error('too late');
|
|
135
|
-
},
|
|
136
|
-
{ maxAttempts: 3, totalTimeout: 1, retryDelay: 5, retryIf: true }
|
|
137
|
-
)
|
|
138
|
-
).rejects.toMatchObject({ code: 'EDEADLINE' });
|
|
139
|
-
});
|
|
140
|
-
});
|