diff --git a/.eslintrc.json b/.eslintrc.json index d30c319..edbe7f0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,7 @@ "no-unused-vars": ["error", { "varsIgnorePattern": "^debug$", "argsIgnorePattern": "^_\\w+" }], "no-use-before-define": 0, "object-curly-newline": 0, - "padded-blocks": 0 + "padded-blocks": 0, + "valid-typeof": 0 } } diff --git a/README.md b/README.md index 710e97e..3c04e1a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ try { | Name | Type | Required | Default | Notes | | -------------------- | ------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | factory | Factory | Y | | An instance of a resource factory. | +| autoStart | boolean | N | false | Initialises the pool automatically. | | minSize | integer | N | 0 | Sets the minimum pool size. | | maxSize | integer | N | Infinity | Sets the maximum pool size. | | maxQueueDepth | integer | N | Infinity | Sets the maximum acquire queue depth, which may be useful to constrain memory usage during exceptionally high peaks. Only meaningful when maxSize is also set. | diff --git a/index.d.ts b/index.d.ts index 21cc1bf..e7cd28a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -14,6 +14,7 @@ export class Pool extends EventEmitter { export type PoolOptions = { factory: Factory; + autoStart?: boolean; minSize?: number; maxSize?: number; acquireTimeout: number; @@ -51,4 +52,3 @@ export namespace Errors { class ResourceValidationFailed extends XPoolError {} class ResourceDestructionFailed extends XPoolError {} } - diff --git a/lib/Pool.js b/lib/Pool.js index 93ec377..beb2400 100644 --- a/lib/Pool.js +++ b/lib/Pool.js @@ -2,10 +2,11 @@ const { EventEmitter } = require('node:events'); const { scheduler } = require('node:timers/promises'); const TimedTask = require('./TimedTask'); const State = require('./State'); -const { validateFactory, validateNumber } = require('./validation'); +const { validateFactory, validateBoolean, validateNumber } = require('./validation'); const { ResourceCreationFailed, ResourceValidationFailed, ResourceDestructionFailed, PoolNotRunning } = require('./Errors'); const { XPoolOperation, InitialisePoolOperation, AcquireResourceOperation, CreateResourceOperation, ValidateResourceOperation, ReleaseResourceOperation, WithResourceOperation, DestroyResourceOperation, EvictBadResourcesOperation, ShutdownPoolOperation, DestroySpareResourcesOperation } = require('./Operations'); +const DEFAULT_AUTO_START = false; const DEFAULT_ACQUIRE_RETRY_INTERVAL = 100; module.exports = class Pool extends EventEmitter { @@ -13,12 +14,15 @@ module.exports = class Pool extends EventEmitter { constructor(options = {}) { super(); this._factory = validateFactory(options.factory); + this._autoStart = validateBoolean('autoStart', options, false) || DEFAULT_AUTO_START; this._acquireTimeout = validateNumber('acquireTimeout', options, true, 1); this._acquireRetryInterval = validateNumber('acquireRetryInterval', options, false, 0) || DEFAULT_ACQUIRE_RETRY_INTERVAL; this._destroyTimeout = validateNumber('destroyTimeout', options, true, 1); this._initialiseTimeout = validateNumber('initialiseTimeout', options, false, 1); this._shutdownTimeout = validateNumber('shutdownTimeout', options, false, 1); this._state = new State({ maxSize: options.maxSize, minSize: options.minSize, maxQueueDepth: options.maxQueueDepth }); + + if (this._autoStart) this.initialise(); } async initialise() { diff --git a/lib/validation.js b/lib/validation.js index 1b10d4e..1569dfc 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -8,12 +8,15 @@ function validateFactory(factory) { return factory; } +function validateBoolean(name, options, mandatory) { + const value = options[name]; + checkMandatory(name, value, mandatory) || checkType(name, value, 'boolean'); + return value; +} + function validateNumber(name, options, mandatory, minValue) { const value = options[name]; - if (mandatory && (value === undefined || value === null)) throw new ConfigurationError(`${name} is a required option`); - if (value === undefined || value === null) return; - if (typeof value !== 'number') throw new ConfigurationError(`The ${name} option must be a number`); - if (value < minValue) throw new ConfigurationError(`The ${name} option must be at least ${minValue}`); + checkMandatory(name, value, mandatory) || (checkType(name, value, 'number') && checkMinValue(name, value, minValue)); return value; } @@ -24,8 +27,24 @@ function validateUpperBoundary(name1, name2, options) { if (value1 > value2) throw new ConfigurationError(`The ${name1} option must be less than or equal to ${name2}`); } +function checkMandatory(name, value, mandatory) { + if (mandatory && (value === undefined || value === null)) throw new ConfigurationError(`${name} is a required option`); + if (value === undefined || value === null) return true; +} + +function checkType(name, value, type) { + if (typeof value !== type) throw new ConfigurationError(`The ${name} option must be a ${type}`); + return true; +} + +function checkMinValue(name, value, minValue) { + if (value < minValue) throw new ConfigurationError(`The ${name} option must be at least ${minValue}`); + return true; +} + module.exports = { validateFactory, + validateBoolean, validateNumber, validateUpperBoundary, }; diff --git a/test/Pool.test.js b/test/Pool.test.js index f17302e..b8103cd 100644 --- a/test/Pool.test.js +++ b/test/Pool.test.js @@ -5,7 +5,7 @@ const { describe, it } = require('zunit'); const TestFactory = require('./lib/TestFactory'); const { Pool, - Operations: { XPoolEvent, CreateResourceOperation, ValidateResourceOperation, ReleaseResourceOperation, DestroyResourceOperation }, + Operations: { XPoolEvent, InitialisePoolOperation, CreateResourceOperation, ValidateResourceOperation, ReleaseResourceOperation, DestroyResourceOperation }, Errors: { XPoolError, ConfigurationError, ResourceCreationFailed, ResourceValidationFailed, ResourceDestructionFailed, OperationTimedout, PoolNotRunning, MaxQueueDepthExceeded }, } = require('../index'); @@ -51,6 +51,31 @@ describe('Pool', () => { }); }); + describe('autoStart', () => { + + it('should require autoStart to be a boolean', () => { + const factory = new TestFactory(); + throws(() => new Pool({ factory, acquireTimeout: 1000, destroyTimeout: 1000, autoStart: 'false' }), (err) => { + eq(err.code, ConfigurationError.code); + eq(err.message, 'The autoStart option must be a boolean. Please read the documentation at https://acuminous.github.io/x-pool'); + return true; + }); + }); + + it('should initialise the pool when set', async (t, done) => { + const resources = ['R1', 'R2', 'R3', 'R4', 'R5']; + const factory = new TestFactory(resources); + const pool = createPool({ factory, minSize: 5, autoStart: true }); + + pool.once(InitialisePoolOperation.SUCCEEDED, () => { + const { size, idle } = pool.stats(); + eq(size, 5); + eq(idle, 5); + done(); + }); + }); + }); + describe('maxSize', () => { it('should require maxSize to be a number', () => { @@ -1256,8 +1281,8 @@ describe('Pool', () => { }); }); -function createPool({ factory, minSize, maxSize, maxQueueDepth, initialiseTimeout, acquireTimeout = 1000, acquireRetryInterval, destroyTimeout = 1000 }) { - const pool = new Pool({ factory, minSize, maxSize, maxQueueDepth, initialiseTimeout, acquireTimeout, acquireRetryInterval, destroyTimeout }); +function createPool({ factory, autoStart, minSize, maxSize, maxQueueDepth, initialiseTimeout, acquireTimeout = 1000, acquireRetryInterval, destroyTimeout = 1000 }) { + const pool = new Pool({ factory, autoStart, minSize, maxSize, maxQueueDepth, initialiseTimeout, acquireTimeout, acquireRetryInterval, destroyTimeout }); pool.on(XPoolEvent, ({ message }) => { debug(message); });