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,100 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerMemoizer } from '../src/helpers/powerCache.js';
|
|
3
|
-
|
|
4
|
-
describe('PowerMemoizer.memoize', () => {
|
|
5
|
-
it('throws when memoize is called with a non-function', () => {
|
|
6
|
-
const pm = new PowerMemoizer();
|
|
7
|
-
expect(() => pm.memoize(null)).toThrow(TypeError);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('memoizes sync function via memoize() and attaches helpers', () => {
|
|
11
|
-
let calls = 0;
|
|
12
|
-
const fn = (x) => {
|
|
13
|
-
calls++;
|
|
14
|
-
return x + 1;
|
|
15
|
-
};
|
|
16
|
-
const pm = new PowerMemoizer();
|
|
17
|
-
const memo = pm.memoize(fn);
|
|
18
|
-
expect(memo(1)).toBe(2);
|
|
19
|
-
expect(calls).toBe(1);
|
|
20
|
-
expect(memo(1)).toBe(2);
|
|
21
|
-
expect(calls).toBe(1);
|
|
22
|
-
// helpers
|
|
23
|
-
expect(typeof memo.get).toBe('function');
|
|
24
|
-
expect(memo.get(1)).toBe(2);
|
|
25
|
-
expect(typeof memo.has).toBe('function');
|
|
26
|
-
expect(memo.has(1)).toBe(true);
|
|
27
|
-
expect(typeof memo.delete).toBe('function');
|
|
28
|
-
expect(memo.delete(1)).toBe(true);
|
|
29
|
-
expect(memo.has(1)).toBe(false);
|
|
30
|
-
expect(typeof memo.stats).toBe('function');
|
|
31
|
-
expect(memo.stats()).toHaveProperty('size');
|
|
32
|
-
expect(memo.cache).toBe(pm.cache);
|
|
33
|
-
expect(memo.original).toBe(fn);
|
|
34
|
-
expect(typeof memo.clear).toBe('function');
|
|
35
|
-
memo.clear();
|
|
36
|
-
expect(memo(1)).toBe(2);
|
|
37
|
-
expect(calls).toBe(2);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('memoized function is instanceof PowerMemoizer', () => {
|
|
41
|
-
const pm = new PowerMemoizer();
|
|
42
|
-
const memo = pm.memoize((x) => x);
|
|
43
|
-
expect(memo instanceof PowerMemoizer).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('honors per-wrapper ttl and weight options', async () => {
|
|
47
|
-
// TTL override
|
|
48
|
-
let calls = 0;
|
|
49
|
-
const fn = (x) => {
|
|
50
|
-
calls++;
|
|
51
|
-
return x;
|
|
52
|
-
};
|
|
53
|
-
const pm = new PowerMemoizer();
|
|
54
|
-
const memo = pm.memoize(fn, { ttl: 5 });
|
|
55
|
-
expect(memo(1)).toBe(1);
|
|
56
|
-
expect(calls).toBe(1);
|
|
57
|
-
expect(memo(1)).toBe(1);
|
|
58
|
-
expect(calls).toBe(1);
|
|
59
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
60
|
-
expect(memo(1)).toBe(1);
|
|
61
|
-
expect(calls).toBe(2);
|
|
62
|
-
|
|
63
|
-
// Weight override with rejection by cache
|
|
64
|
-
let calls2 = 0;
|
|
65
|
-
const fn2 = (x) => {
|
|
66
|
-
calls2++;
|
|
67
|
-
return { v: x };
|
|
68
|
-
};
|
|
69
|
-
const pm2 = new PowerMemoizer(undefined, {
|
|
70
|
-
cacheOptions: { maxWeight: 1, rejectOversized: true },
|
|
71
|
-
});
|
|
72
|
-
const memo2 = pm2.memoize(fn2, { weight: 2 });
|
|
73
|
-
// first call returns value but should not be cached due to oversized weight
|
|
74
|
-
const a = memo2(1);
|
|
75
|
-
expect(a).toEqual({ v: 1 });
|
|
76
|
-
expect(calls2).toBe(1);
|
|
77
|
-
// second call should invoke original again because caching was rejected
|
|
78
|
-
const b = memo2(1);
|
|
79
|
-
expect(b).toEqual({ v: 1 });
|
|
80
|
-
expect(calls2).toBe(2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('uses constructor default memoize options when per-call ttl/weight are omitted', async () => {
|
|
84
|
-
let calls = 0;
|
|
85
|
-
const pm = new PowerMemoizer(undefined, { ttl: 5 });
|
|
86
|
-
const memo = pm.memoize((value) => {
|
|
87
|
-
calls += 1;
|
|
88
|
-
return value;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
expect(memo('x')).toBe('x');
|
|
92
|
-
expect(memo('x')).toBe('x');
|
|
93
|
-
expect(calls).toBe(1);
|
|
94
|
-
|
|
95
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
96
|
-
|
|
97
|
-
expect(memo('x')).toBe('x');
|
|
98
|
-
expect(calls).toBe(2);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerMemoizer } from '../src/helpers/powerCache.js';
|
|
3
|
-
|
|
4
|
-
describe('PowerMemoizer', () => {
|
|
5
|
-
it('caches sync function results', () => {
|
|
6
|
-
let calls = 0;
|
|
7
|
-
const fn = (a) => {
|
|
8
|
-
calls++;
|
|
9
|
-
return a * 2;
|
|
10
|
-
};
|
|
11
|
-
const pm = new PowerMemoizer();
|
|
12
|
-
const memo = pm.memoize(fn);
|
|
13
|
-
expect(memo(2)).toBe(4);
|
|
14
|
-
expect(calls).toBe(1);
|
|
15
|
-
expect(memo(2)).toBe(4);
|
|
16
|
-
expect(calls).toBe(1);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('dedupes concurrent async calls and caches result', async () => {
|
|
20
|
-
let calls = 0;
|
|
21
|
-
const fn = async (x) => {
|
|
22
|
-
calls++;
|
|
23
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
24
|
-
return x * 3;
|
|
25
|
-
};
|
|
26
|
-
const pm = new PowerMemoizer();
|
|
27
|
-
const memo = pm.memoize(fn);
|
|
28
|
-
const p1 = memo(3);
|
|
29
|
-
const p2 = memo(3);
|
|
30
|
-
// same in-flight promise
|
|
31
|
-
expect(p1).toBe(p2);
|
|
32
|
-
const res = await p1;
|
|
33
|
-
expect(res).toBe(9);
|
|
34
|
-
expect(calls).toBe(1);
|
|
35
|
-
// subsequent call returns cached value (not a new invocation)
|
|
36
|
-
const r2 = await Promise.resolve(memo(3));
|
|
37
|
-
expect(r2).toBe(9);
|
|
38
|
-
expect(calls).toBe(1);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('does not cache rejected promises', async () => {
|
|
42
|
-
let calls = 0;
|
|
43
|
-
const fn = async () => {
|
|
44
|
-
calls++;
|
|
45
|
-
await new Promise((r) => setTimeout(r, 1));
|
|
46
|
-
throw new Error('boom');
|
|
47
|
-
};
|
|
48
|
-
const pm = new PowerMemoizer();
|
|
49
|
-
const memo = pm.memoize(fn);
|
|
50
|
-
await expect(memo()).rejects.toThrow('boom');
|
|
51
|
-
await expect(memo()).rejects.toThrow('boom');
|
|
52
|
-
expect(calls).toBe(2);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('honors TTL and expires entries', async () => {
|
|
56
|
-
let calls = 0;
|
|
57
|
-
const fn = (x) => {
|
|
58
|
-
calls++;
|
|
59
|
-
return x;
|
|
60
|
-
};
|
|
61
|
-
const pm = new PowerMemoizer(undefined, { ttl: 5 });
|
|
62
|
-
const memo = pm.memoize(fn);
|
|
63
|
-
expect(memo(1)).toBe(1);
|
|
64
|
-
expect(calls).toBe(1);
|
|
65
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
66
|
-
expect(memo(1)).toBe(1);
|
|
67
|
-
expect(calls).toBe(2);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('delete and clear remove cached entries', () => {
|
|
71
|
-
let calls = 0;
|
|
72
|
-
const fn = (x) => {
|
|
73
|
-
calls++;
|
|
74
|
-
return x;
|
|
75
|
-
};
|
|
76
|
-
const pm = new PowerMemoizer();
|
|
77
|
-
const memo = pm.memoize(fn);
|
|
78
|
-
expect(memo(5)).toBe(5);
|
|
79
|
-
memo.delete(5);
|
|
80
|
-
expect(memo(5)).toBe(5);
|
|
81
|
-
expect(calls).toBe(2);
|
|
82
|
-
memo.clear();
|
|
83
|
-
expect(memo(6)).toBe(6);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerObserver } from '../src/helpers/powerObserver.js';
|
|
3
|
-
|
|
4
|
-
describe('PowerObserver', () => {
|
|
5
|
-
it('notifies subscribers asynchronously and returns unsubscribe', async () => {
|
|
6
|
-
const obs = new PowerObserver(1);
|
|
7
|
-
let called = 0;
|
|
8
|
-
const unsub = obs.subscribe((next, prev) => {
|
|
9
|
-
expect(prev).toBe(1);
|
|
10
|
-
expect(next).toBe(2);
|
|
11
|
-
called++;
|
|
12
|
-
});
|
|
13
|
-
obs.value = 2;
|
|
14
|
-
await Promise.resolve();
|
|
15
|
-
expect(called).toBe(1);
|
|
16
|
-
unsub();
|
|
17
|
-
obs.value = 3;
|
|
18
|
-
await Promise.resolve();
|
|
19
|
-
expect(called).toBe(1);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('clear removes all subscribers and size reflects count', () => {
|
|
23
|
-
const obs = new PowerObserver('a');
|
|
24
|
-
const sub = () => {};
|
|
25
|
-
obs.subscribe(sub);
|
|
26
|
-
obs.subscribe(() => {});
|
|
27
|
-
expect(obs.size).toBe(2);
|
|
28
|
-
obs.clear();
|
|
29
|
-
expect(obs.size).toBe(0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('throws on invalid subscriber', () => {
|
|
33
|
-
const obs = new PowerObserver(0);
|
|
34
|
-
expect(() => obs.subscribe(null)).toThrow();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('respects distinct and map options', async () => {
|
|
38
|
-
const obs = new PowerObserver(2, { distinct: true, map: (v) => v % 2 });
|
|
39
|
-
let calls = 0;
|
|
40
|
-
obs.subscribe(() => {
|
|
41
|
-
calls++;
|
|
42
|
-
});
|
|
43
|
-
obs.value = 4; // maps 0 -> previous 0 -> no notify
|
|
44
|
-
await Promise.resolve();
|
|
45
|
-
expect(calls).toBe(0);
|
|
46
|
-
obs.value = 5; // maps 1 -> notify
|
|
47
|
-
await Promise.resolve();
|
|
48
|
-
expect(calls).toBe(1);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('allows setting map function via .map()', async () => {
|
|
52
|
-
const obs = new PowerObserver(1);
|
|
53
|
-
obs.map((v) => v * 2);
|
|
54
|
-
let called = 0;
|
|
55
|
-
obs.subscribe((n, p) => {
|
|
56
|
-
expect(n).toBe(4);
|
|
57
|
-
expect(p).toBe(2);
|
|
58
|
-
called++;
|
|
59
|
-
});
|
|
60
|
-
obs.value = 2;
|
|
61
|
-
await Promise.resolve();
|
|
62
|
-
expect(called).toBe(1);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('supports sync delivery and swallows subscriber errors', () => {
|
|
66
|
-
const obs = new PowerObserver(1, { async: false });
|
|
67
|
-
const seen = [];
|
|
68
|
-
obs.subscribe(() => {
|
|
69
|
-
throw new Error('listener failed');
|
|
70
|
-
});
|
|
71
|
-
obs.subscribe((next, prev) => {
|
|
72
|
-
seen.push([prev, next]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
obs.value = 2;
|
|
76
|
-
|
|
77
|
-
expect(seen).toEqual([[1, 2]]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('coalesces multiple async writes into the latest next value while preserving the first prev', async () => {
|
|
81
|
-
const obs = new PowerObserver(1);
|
|
82
|
-
const seen = [];
|
|
83
|
-
obs.subscribe((next, prev) => {
|
|
84
|
-
seen.push([prev, next]);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
obs.value = 2;
|
|
88
|
-
obs.value = 3;
|
|
89
|
-
await Promise.resolve();
|
|
90
|
-
|
|
91
|
-
expect(seen).toEqual([[1, 3]]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('drain aliases flush and map(null) clears the mapping function', async () => {
|
|
95
|
-
const obs = new PowerObserver(2, { async: 'macrotask', map: (v) => v * 2 });
|
|
96
|
-
const seen = [];
|
|
97
|
-
obs.subscribe((next, prev) => {
|
|
98
|
-
seen.push([prev, next]);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
obs.map(null);
|
|
102
|
-
obs.value = 4;
|
|
103
|
-
obs.drain();
|
|
104
|
-
|
|
105
|
-
expect(seen).toEqual([[2, 4]]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('throws when map is set to a non-function value', () => {
|
|
109
|
-
const obs = new PowerObserver(1);
|
|
110
|
-
expect(() => obs.map(123)).toThrow('map must be a function');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('supports macrotask scheduling and flush()', async () => {
|
|
114
|
-
const obs = new PowerObserver(1, { async: 'macrotask' });
|
|
115
|
-
let called = 0;
|
|
116
|
-
obs.subscribe((n, p) => {
|
|
117
|
-
expect(p).toBe(1);
|
|
118
|
-
expect(n).toBe(2);
|
|
119
|
-
called++;
|
|
120
|
-
});
|
|
121
|
-
obs.value = 2;
|
|
122
|
-
// macrotask: microtask await won't observe it
|
|
123
|
-
await Promise.resolve();
|
|
124
|
-
expect(called).toBe(0);
|
|
125
|
-
// flush synchronously
|
|
126
|
-
obs.flush();
|
|
127
|
-
expect(called).toBe(1);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerPermitGate } from '../src/helpers/powerPermitGate.js';
|
|
3
|
-
|
|
4
|
-
describe('PowerPermitGate', () => {
|
|
5
|
-
it('acquires permits immediately when available', async () => {
|
|
6
|
-
const gate = new PowerPermitGate({ capacity: 2 });
|
|
7
|
-
const release = await gate.acquire();
|
|
8
|
-
expect(typeof release).toBe('function');
|
|
9
|
-
expect(gate.available).toBe(1);
|
|
10
|
-
expect(gate.pending).toBe(0);
|
|
11
|
-
release();
|
|
12
|
-
expect(gate.available).toBe(2);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('queues callers when permits are exhausted', async () => {
|
|
16
|
-
const gate = new PowerPermitGate({ capacity: 1 });
|
|
17
|
-
const firstRelease = await gate.acquire();
|
|
18
|
-
const pending = gate.acquire();
|
|
19
|
-
expect(gate.pending).toBe(1);
|
|
20
|
-
let resolved = false;
|
|
21
|
-
|
|
22
|
-
const promise = pending.then((release) => {
|
|
23
|
-
resolved = true;
|
|
24
|
-
release();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
expect(resolved).toBe(false);
|
|
28
|
-
firstRelease();
|
|
29
|
-
await promise;
|
|
30
|
-
expect(gate.pending).toBe(0);
|
|
31
|
-
expect(resolved).toBe(true);
|
|
32
|
-
expect(gate.available).toBe(1);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('rejects acquire when the wait queue is full', async () => {
|
|
36
|
-
const gate = new PowerPermitGate({ capacity: 1, queueCapacity: 1 });
|
|
37
|
-
await gate.acquire();
|
|
38
|
-
gate.acquire();
|
|
39
|
-
await expect(gate.acquire()).rejects.toThrow('PowerPermitGate queue is full');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('tryAcquire returns null when no permit is available', () => {
|
|
43
|
-
const gate = new PowerPermitGate({ capacity: 1 });
|
|
44
|
-
const release = gate.tryAcquire();
|
|
45
|
-
expect(typeof release).toBe('function');
|
|
46
|
-
expect(gate.tryAcquire()).toBeNull();
|
|
47
|
-
release();
|
|
48
|
-
expect(gate.tryAcquire()).not.toBeNull();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('reset clears waiters and restores available permits', async () => {
|
|
52
|
-
const gate = new PowerPermitGate({ capacity: 1, queueCapacity: 2 });
|
|
53
|
-
const firstRelease = await gate.acquire();
|
|
54
|
-
const pending = gate.acquire();
|
|
55
|
-
let rejected = false;
|
|
56
|
-
pending.catch((err) => {
|
|
57
|
-
rejected = err && err.message === 'PowerPermitGate reset';
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
gate.reset({ available: 1, reason: new Error('PowerPermitGate reset') });
|
|
61
|
-
expect(gate.available).toBe(1);
|
|
62
|
-
await Promise.resolve();
|
|
63
|
-
expect(rejected).toBe(true);
|
|
64
|
-
firstRelease();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { PowerPool } from '../src/helpers/powerPool.js';
|
|
3
|
-
|
|
4
|
-
describe('PowerPool auto-transfer behavior', () => {
|
|
5
|
-
it('postMessage without transfer encodes object and passes ArrayBuffer in transfer list', async () => {
|
|
6
|
-
class MockUnderlying {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.onmessage = null;
|
|
9
|
-
this.postMessage = vi.fn((msg) => {
|
|
10
|
-
// echo back after a tick to simulate worker reply
|
|
11
|
-
setTimeout(() => {
|
|
12
|
-
if (this.onmessage) this.onmessage({ data: msg });
|
|
13
|
-
}, 0);
|
|
14
|
-
});
|
|
15
|
-
this.terminate = vi.fn();
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const pool = new PowerPool(MockUnderlying, { size: 1, idleTimeout: 1000 });
|
|
20
|
-
try {
|
|
21
|
-
const sent = { hello: 'x'.repeat(2000) };
|
|
22
|
-
const p = new Promise((resolve) => {
|
|
23
|
-
pool.onmessage = (e) => resolve(e.data);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const ok = pool.postMessage(sent);
|
|
27
|
-
expect(ok).toBe(true);
|
|
28
|
-
|
|
29
|
-
// wait for reply
|
|
30
|
-
const decoded = await p;
|
|
31
|
-
expect(decoded).toEqual(sent);
|
|
32
|
-
|
|
33
|
-
const underlying = pool.workers[0].worker._underlying || pool.workers[0]._underlying;
|
|
34
|
-
expect(underlying).toBeTruthy();
|
|
35
|
-
const call = underlying.postMessage.mock.calls[0];
|
|
36
|
-
expect(call).toBeTruthy();
|
|
37
|
-
const [argMsg, argTransfer] = call;
|
|
38
|
-
// message should be a Uint8Array and transfer should include its buffer
|
|
39
|
-
expect(argMsg).toBeInstanceOf(Uint8Array);
|
|
40
|
-
expect(Array.isArray(argTransfer)).toBe(true);
|
|
41
|
-
expect(argTransfer.length).toBeGreaterThanOrEqual(1);
|
|
42
|
-
expect(argTransfer[0]).toBe(argMsg.buffer);
|
|
43
|
-
} finally {
|
|
44
|
-
pool.terminate();
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('postMessage with explicit empty transfer array still forwards the encoded buffer', () => {
|
|
49
|
-
class MockUnderlying {
|
|
50
|
-
constructor() {
|
|
51
|
-
this.onmessage = null;
|
|
52
|
-
this.postMessage = vi.fn();
|
|
53
|
-
this.terminate = vi.fn();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const pool = new PowerPool(MockUnderlying, { size: 1, idleTimeout: 1000 });
|
|
58
|
-
try {
|
|
59
|
-
const ok = pool.postMessage({ hello: 'world' }, []);
|
|
60
|
-
expect(ok).toBe(true);
|
|
61
|
-
|
|
62
|
-
const underlying = pool.workers[0].worker._underlying || pool.workers[0]._underlying;
|
|
63
|
-
expect(underlying).toBeTruthy();
|
|
64
|
-
const call = underlying.postMessage.mock.calls[0];
|
|
65
|
-
expect(call).toBeTruthy();
|
|
66
|
-
const [argMsg, argTransfer] = call;
|
|
67
|
-
expect(argMsg).toBeInstanceOf(Uint8Array);
|
|
68
|
-
expect(argTransfer).toEqual([argMsg.buffer]);
|
|
69
|
-
} finally {
|
|
70
|
-
pool.terminate();
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('broadcast without transfer encodes per-worker and provides transferable buffers', () => {
|
|
75
|
-
class MockUnderlying {
|
|
76
|
-
constructor() {
|
|
77
|
-
this.onmessage = null;
|
|
78
|
-
this.postMessage = vi.fn();
|
|
79
|
-
this.terminate = vi.fn();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const pool = new PowerPool(MockUnderlying, { size: 3, idleTimeout: 1000 });
|
|
84
|
-
try {
|
|
85
|
-
pool.broadcast({ ping: 'pong'.repeat(500) });
|
|
86
|
-
for (const w of pool.workers) {
|
|
87
|
-
const underlying = w.worker._underlying || w._underlying;
|
|
88
|
-
expect(underlying).toBeTruthy();
|
|
89
|
-
const call = underlying.postMessage.mock.calls[0];
|
|
90
|
-
expect(call).toBeTruthy();
|
|
91
|
-
const [argMsg, argTransfer] = call;
|
|
92
|
-
expect(argMsg).toBeInstanceOf(Uint8Array);
|
|
93
|
-
expect(Array.isArray(argTransfer)).toBe(true);
|
|
94
|
-
expect(argTransfer[0]).toBe(argMsg.buffer);
|
|
95
|
-
}
|
|
96
|
-
} finally {
|
|
97
|
-
pool.terminate();
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerPool } from '../src/helpers/powerPool.js';
|
|
3
|
-
|
|
4
|
-
// Lightweight mock underlying that reports a fixed processing duration
|
|
5
|
-
class MockUnderlyingWithDuration {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.onmessage = null;
|
|
8
|
-
this.postMessage = () => {
|
|
9
|
-
// respond on next tick with a reported duration
|
|
10
|
-
setTimeout(() => {
|
|
11
|
-
if (this.onmessage)
|
|
12
|
-
this.onmessage({ data: { duration: MockUnderlyingWithDuration.responseDuration } });
|
|
13
|
-
}, 1);
|
|
14
|
-
};
|
|
15
|
-
this.terminate = () => {};
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('PowerPool autoscale - extra behaviors', () => {
|
|
20
|
-
it('multi-step scaling: adds up to `stepUp` workers in one tick', async () => {
|
|
21
|
-
MockUnderlyingWithDuration.responseDuration = 200;
|
|
22
|
-
|
|
23
|
-
const pool = new PowerPool(MockUnderlyingWithDuration, {
|
|
24
|
-
size: 1,
|
|
25
|
-
minSize: 1,
|
|
26
|
-
maxSize: 8,
|
|
27
|
-
lazy: false,
|
|
28
|
-
taskQueue: true,
|
|
29
|
-
autoScale: {
|
|
30
|
-
intervalMs: 50,
|
|
31
|
-
targetMs: 50,
|
|
32
|
-
alpha: 0.5,
|
|
33
|
-
cooldownMs: 10,
|
|
34
|
-
hysteresis: 0.1,
|
|
35
|
-
stepUp: 3,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
// post many tasks to ensure EWMA rises
|
|
41
|
-
for (let i = 0; i < 12; i++) pool.postMessage({ i });
|
|
42
|
-
|
|
43
|
-
// wait several ticks
|
|
44
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
45
|
-
|
|
46
|
-
// should have added at least stepUp workers in a single tick
|
|
47
|
-
expect(pool.workers.length).toBeGreaterThanOrEqual(1 + 3);
|
|
48
|
-
} finally {
|
|
49
|
-
pool.terminate();
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('backoff: backoff multiplier increases after scale action', async () => {
|
|
54
|
-
MockUnderlyingWithDuration.responseDuration = 10;
|
|
55
|
-
|
|
56
|
-
const pool = new PowerPool(MockUnderlyingWithDuration, {
|
|
57
|
-
size: 8,
|
|
58
|
-
minSize: 1,
|
|
59
|
-
maxSize: 8,
|
|
60
|
-
lazy: false,
|
|
61
|
-
taskQueue: true,
|
|
62
|
-
autoScale: {
|
|
63
|
-
intervalMs: 50,
|
|
64
|
-
targetMs: 50,
|
|
65
|
-
alpha: 0.5,
|
|
66
|
-
cooldownMs: 50,
|
|
67
|
-
hysteresis: 0.1,
|
|
68
|
-
backoffFactor: 4,
|
|
69
|
-
backoffMaxMultiplier: 8,
|
|
70
|
-
backoffResetMs: 1000,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// Start at max size so normal auto-growth cannot add workers.
|
|
76
|
-
for (let i = 0; i < 8; i++) pool.postMessage({ i });
|
|
77
|
-
|
|
78
|
-
// wait enough time for at least one scale action
|
|
79
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
80
|
-
|
|
81
|
-
// internal multiplier should have increased from 1 when autoscale scaled down
|
|
82
|
-
expect(pool._autoScaleBackoffMultiplier).toBeGreaterThanOrEqual(1);
|
|
83
|
-
expect(pool._autoScaleBackoffMultiplier).toBeGreaterThanOrEqual(4);
|
|
84
|
-
} finally {
|
|
85
|
-
pool.terminate();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { PowerPool } from '../src/helpers/powerPool.js';
|
|
3
|
-
|
|
4
|
-
// Lightweight mock underlying used for the first autoscale test
|
|
5
|
-
class MockUnderlying {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.onmessage = null;
|
|
8
|
-
this.postMessage = () => {};
|
|
9
|
-
this.terminate = () => {};
|
|
10
|
-
}
|
|
11
|
-
addEventListener(type, cb) {
|
|
12
|
-
if (type === 'message') this.onmessage = cb;
|
|
13
|
-
}
|
|
14
|
-
removeEventListener() {}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('PowerPool autoscale', () => {
|
|
18
|
-
it('does not terminate workers with in-flight tasks during scale-down', () => {
|
|
19
|
-
const pool = new PowerPool(MockUnderlying, {
|
|
20
|
-
size: 2,
|
|
21
|
-
minSize: 0,
|
|
22
|
-
maxSize: 2,
|
|
23
|
-
lazy: false,
|
|
24
|
-
maxTasksPerWorker: 1,
|
|
25
|
-
idleTimeout: 1000,
|
|
26
|
-
});
|
|
27
|
-
try {
|
|
28
|
-
// simulate autoscale config without starting an interval
|
|
29
|
-
pool._autoScale = {
|
|
30
|
-
enabled: true,
|
|
31
|
-
intervalMs: 1000,
|
|
32
|
-
targetMs: 100,
|
|
33
|
-
alpha: 0.2,
|
|
34
|
-
cooldownMs: 100,
|
|
35
|
-
hysteresis: 0.5,
|
|
36
|
-
stepUp: 1,
|
|
37
|
-
stepDown: 2,
|
|
38
|
-
backoffFactor: 1,
|
|
39
|
-
backoffMaxMultiplier: 1,
|
|
40
|
-
backoffResetMs: 10000,
|
|
41
|
-
};
|
|
42
|
-
pool._autoScaleBackoffMultiplier = 1;
|
|
43
|
-
|
|
44
|
-
// set EWMA low to request scale-down
|
|
45
|
-
pool._ewmaLatency = 1;
|
|
46
|
-
|
|
47
|
-
// mark both workers as busy
|
|
48
|
-
pool.workers[0].tasks = 1;
|
|
49
|
-
pool.workers[1].tasks = 1;
|
|
50
|
-
|
|
51
|
-
pool._autoScaleTick();
|
|
52
|
-
// no idle workers -> none removed
|
|
53
|
-
expect(pool.workers.length).toBe(2);
|
|
54
|
-
|
|
55
|
-
// make one worker idle - it should be eligible for removal
|
|
56
|
-
pool.workers[1].tasks = 0;
|
|
57
|
-
pool._autoScaleTick();
|
|
58
|
-
expect(pool.workers.length).toBe(1);
|
|
59
|
-
} finally {
|
|
60
|
-
pool.terminate();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Lightweight mock underlying that reports a fixed processing duration
|
|
66
|
-
class MockUnderlyingWithDuration {
|
|
67
|
-
constructor() {
|
|
68
|
-
this.onmessage = null;
|
|
69
|
-
this.postMessage = () => {
|
|
70
|
-
// respond on next tick with a reported duration
|
|
71
|
-
setTimeout(() => {
|
|
72
|
-
if (this.onmessage)
|
|
73
|
-
this.onmessage({ data: { duration: MockUnderlyingWithDuration.responseDuration } });
|
|
74
|
-
}, 1);
|
|
75
|
-
};
|
|
76
|
-
this.terminate = () => {};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
describe('PowerPool autoscale', () => {
|
|
81
|
-
it('scales up when observed EWMA latency exceeds target', async () => {
|
|
82
|
-
// heavy tasks reported as 200ms each
|
|
83
|
-
MockUnderlyingWithDuration.responseDuration = 200;
|
|
84
|
-
|
|
85
|
-
const pool = new PowerPool(MockUnderlyingWithDuration, {
|
|
86
|
-
size: 1,
|
|
87
|
-
minSize: 1,
|
|
88
|
-
maxSize: 4,
|
|
89
|
-
lazy: false,
|
|
90
|
-
taskQueue: true,
|
|
91
|
-
// aggressive autoscale tick for tests
|
|
92
|
-
autoScale: { intervalMs: 50, targetMs: 50, alpha: 0.5, cooldownMs: 100, hysteresis: 0.1 },
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
// post multiple tasks to accumulate EWMA
|
|
97
|
-
for (let i = 0; i < 6; i++) pool.postMessage({ i });
|
|
98
|
-
|
|
99
|
-
// wait enough time for several autoscale ticks
|
|
100
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
101
|
-
|
|
102
|
-
// pool should have scaled up at least one worker
|
|
103
|
-
expect(pool.workers.length).toBeGreaterThan(1);
|
|
104
|
-
} finally {
|
|
105
|
-
pool.terminate();
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('scales down when latency is low and queue is empty', async () => {
|
|
110
|
-
// light tasks reported as 1ms each
|
|
111
|
-
MockUnderlyingWithDuration.responseDuration = 1;
|
|
112
|
-
|
|
113
|
-
const pool = new PowerPool(MockUnderlyingWithDuration, {
|
|
114
|
-
size: 3,
|
|
115
|
-
minSize: 1,
|
|
116
|
-
maxSize: 4,
|
|
117
|
-
lazy: false,
|
|
118
|
-
taskQueue: true,
|
|
119
|
-
autoScale: { intervalMs: 50, targetMs: 50, alpha: 0.5, cooldownMs: 100, hysteresis: 0.1 },
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
// run a few quick tasks to seed a low EWMA
|
|
124
|
-
for (let i = 0; i < 4; i++) pool.postMessage({ i });
|
|
125
|
-
|
|
126
|
-
// allow tasks to complete and autoscaler to observe low latencies
|
|
127
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
128
|
-
|
|
129
|
-
// after cooldown and empty queue, pool should have shrunk toward minSize
|
|
130
|
-
expect(pool.workers.length).toBeLessThanOrEqual(2);
|
|
131
|
-
expect(pool.workers.length).toBeGreaterThanOrEqual(1);
|
|
132
|
-
} finally {
|
|
133
|
-
pool.terminate();
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
});
|