@v4fire/core
Version:
V4Fire core library
548 lines (445 loc) • 13.2 kB
text/typescript
/*!
* V4Fire Core
* https://github.com/V4Fire/Core
*
* Released under the MIT license
* https://github.com/V4Fire/Core/blob/master/LICENSE
*/
/**
* [[include:core/pool/README.md]]
* @packageDocumentation
*/
import { EventEmitter2 as EventEmitter } from 'eventemitter2';
import { resolveAfterEvents } from 'core/event';
import SyncPromise from 'core/promise/sync';
import { generate, serialize } from 'core/uuid';
import { Queue } from 'core/queue';
import { hashVal, borrowCounter } from 'core/pool/const';
import type {
Args,
HashFn,
Resource,
ResourceHook,
ResourceFactory,
ResourceDestructor,
PoolHook,
PoolOptions,
WrappedResource,
OptionalWrappedResource
} from 'core/pool/interface';
export * from 'core/pool/const';
export * from 'core/pool/interface';
/**
* Implementation of an object pool structure
* @typeparam T - pool resource
*/
export default class Pool<T = unknown> {
/**
* The maximum number of resources that the pool can contain
*/
readonly maxSize: number = Infinity;
/**
* Number of resources that are stored in the pool
*/
get size(): number {
return this.availableResources.size + this.unavailableResources.size;
}
/**
* Number of available resources that are stored in the pool
*/
get available(): number {
return this.availableResources.size;
}
/**
* A factory to create a new resource for the pool.
* The function take arguments that are passed to `takeOrCreate`, `borrowAndCreate`, etc.
*/
protected resourceFactory: ResourceFactory<T>;
/**
* A function to destroy one resource from the pool
*/
protected resourceDestructor?: ResourceDestructor<T>;
/**
* A function to calculate a hash string for the specified arguments
*/
protected hashFn: HashFn;
/**
* Event emitter to broadcast pool events
* @see [[EventEmitter]]
*/
protected emitter: EventEmitter = new EventEmitter();
/**
* Store of pool resources
*/
protected resourceStore: Map<string, Array<Resource<T>>> = new Map();
/**
* Store of borrowed pool resources
*/
protected borrowedResourceStore: Map<string, Resource<T>> = new Map();
/**
* Set of all available resources
*/
protected availableResources: Set<Resource<Resource<T>>> = new Set();
/**
* Set of all unavailable resources
*/
protected unavailableResources: Set<Resource<Resource<T>>> = new Set();
/**
* Queue of active events
*/
protected events: Map<string, Queue<string>> = new Map();
/**
* Map of active borrow events
*/
protected borrowEventsInQueue: Map<string, string> = new Map();
/**
* Handler: taking some resource via `take` methods
*/
protected onTake?: ResourceHook<T>;
/**
* Handler: taking some resource via `borrow` methods
*/
protected onBorrow?: ResourceHook<T>;
/**
* Handler: releasing of some resource
*/
protected onFree?: ResourceHook<T>;
/**
* Handler: clearing of all pool resources
*/
protected onClear?: PoolHook<T>;
/**
* @param resourceFactory
* @param args - extra arguments to pass to the resource factory during initialization
* @param [opts] - additional options
*/
constructor(
resourceFactory: ResourceFactory<T>,
args: Args,
opts?: PoolOptions<T>
);
/**
* @param resourceFactory
* @param [opts] - additional options
*/
constructor(resourceFactory: ResourceFactory<T>, opts?: PoolOptions<T>);
constructor(
resourceFactory: ResourceFactory<T>,
argsOrOpts?: Args | PoolOptions<T>,
opts?: PoolOptions<T>
) {
const
p: PoolOptions<T> = {...opts};
let
args: unknown[] | Function = [];
if (Object.isArray(argsOrOpts) || Object.isFunction(argsOrOpts)) {
args = argsOrOpts;
} else if (Object.isDictionary(argsOrOpts)) {
Object.assign(p, argsOrOpts);
}
Object.assign(this, Object.reject(p, 'size'));
this.hashFn ??= (() => '[[DEFAULT]]');
this.resourceFactory = resourceFactory;
const
size = p.size ?? 0;
for (let i = 0; i < size; i++) {
this.createResource(...Object.isFunction(args) ? args(i) : args);
}
}
/**
* Returns an available resource from the pool.
* The passed arguments will be used to calculate a resource hash. Also, they will be provided to hook handlers.
*
* The returned result is wrapped with a structure that contains methods to release or drop this resource.
* If the pool is empty, the structure value field will be nullish.
*
* @param [args]
*/
take(...args: unknown[]): OptionalWrappedResource<T> {
const
resource = this.resourceStore.get(this.hashFn(...args))?.pop();
if (resource == null) {
return this.wrapResource(null);
}
resource[borrowCounter]++;
this.availableResources.delete(resource);
this.unavailableResources.add(resource);
if (this.onTake != null) {
this.onTake(resource, this, ...args);
}
return this.wrapResource(resource);
}
/**
* Returns an available resource from the pool.
* The passed arguments will be used to calculate a resource hash. Also, they will be provided to hook handlers.
*
* The returned result is wrapped with a structure that contains methods to release or drop this resource.
* If the pool is empty, it creates a new resource and returns it.
*
* @param [args]
*/
takeOrCreate(...args: unknown[]): WrappedResource<T> {
if (this.canTake(...args) === 0) {
this.createResource(...args);
}
return Object.cast(this.take(...args));
}
/**
* Returns a promise with an available resource from the pull.
* The passed arguments will be used to calculate a resource hash. Also, they will be provided to hook handlers.
*
* The returned result is wrapped with a structure that contains methods to release or drop this resource.
* If the pool is empty, the promise will wait till it release.
*
* @param [args]
*/
takeOrWait(...args: unknown[]): SyncPromise<WrappedResource<T>> {
const
event = this.hashFn(...args),
id = serialize(generate());
return new SyncPromise((r) => {
if (this.canTake(...args) !== 0) {
r(this.take(...args));
return;
}
let
queue = this.events.get(event);
if (queue == null) {
queue = new Queue();
this.events.set(event, queue);
}
queue.push(id);
r(resolveAfterEvents(this.emitter, id).then(() => this.take(...args)));
});
}
/**
* Borrows an available resource from the pool.
* The passed arguments will be used to calculate a resource hash. Also, they will be provided to hook handlers.
*
* When a resource is borrowed, it won’t be dropped from the pool. I.e. you can share it with other consumers.
* Mind, you can’t take this resource from the pool when it’s borrowed.
*
* The returned result is wrapped with a structure that contains methods to release or drop this resource.
* If the pool is empty, the structure value field will be nullish.
*
* @param [args]
*/
borrow(...args: unknown[]): OptionalWrappedResource<T> {
const
hash = this.hashFn(...args);
let resource = this.borrowedResourceStore.get(hash);
resource = resource ?? this.resourceStore.get(hash)?.pop();
if (resource == null) {
return this.wrapResource(null);
}
this.borrowedResourceStore.set(hash, resource);
resource[borrowCounter]++;
if (this.onBorrow != null) {
this.onBorrow(resource, this, ...args);
}
return this.wrapResource(resource);
}
/**
* Borrows an available resource from the pool.
* The passed arguments will be used to calculate a resource hash. Also, they will be provided to hook handlers.
*
* When a resource is borrowed, it won’t be dropped from the pool. I.e. you can share it with other consumers.
* Mind, you can’t take this resource from the pool when it’s borrowed.
*
* The returned result is wrapped with a structure that contains methods to release or drop this resource.
* If the pool is empty, it creates a new resource and returns it.
*
* @param [args]
*/
borrowOrCreate(...args: unknown[]): WrappedResource<T> {
if (!this.canBorrow(...args)) {
this.createResource(...args);
}
return Object.cast(this.borrow(...args));
}
/**
* Returns a promise with a borrowed resource from the pull.
* The passed arguments will be used to calculate a resource hash. Also, they will be provided to hook handlers.
*
* When a resource is borrowed, it won’t be dropped from the pool. I.e. you can share it with other consumers.
* Mind, you can’t take this resource from the pool when it’s borrowed.
*
* The returned result is wrapped with a structure that contains methods to release or drop this resource.
* If the pool is empty, the promise will wait till it release.
*
* @param [args]
*/
borrowOrWait(...args: unknown[]): SyncPromise<WrappedResource<T>> {
const
event = this.hashFn(...args);
let
id = serialize(generate());
return new SyncPromise((r) => {
if (this.canBorrow(...args)) {
r(this.borrow(...args));
return;
}
const
events = this.borrowEventsInQueue.get(event);
if (events == null) {
if (!this.events.has(event)) {
this.events.set(event, new Queue());
}
this.events.get(event)?.push(id);
this.borrowEventsInQueue.set(event, id);
} else {
id = events;
}
r(resolveAfterEvents(this.emitter, id).then(() => {
this.borrowEventsInQueue.delete(event);
return this.borrow(...args);
}));
});
}
/**
* Clears the pool, i.e. drops all created resource.
* The method takes arguments that will be provided to hook handlers.
*
* @param [args]
*/
clear(...args: unknown[]): void {
const
destructor = this.resourceDestructor;
if (destructor != null) {
this.availableResources.forEach((resource) => {
destructor(resource);
});
this.unavailableResources.forEach((resource) => {
destructor(resource);
});
}
this.resourceStore.clear();
this.borrowedResourceStore.clear();
this.availableResources.clear();
this.unavailableResources.clear();
if (this.onClear != null) {
this.onClear(this, ...args);
}
}
/**
* Returns how many elements of the specified kind you can take.
* The method takes arguments that will be used to calculate a resource hash.
*
* @param [args]
*/
protected canTake(...args: unknown[]): number {
const array = this.resourceStore.get(this.hashFn(...args));
return Object.size(array);
}
/**
* Checks if you can borrow a resource.
* The passed arguments will be used to calculate a resource hash.
*
* @param [args]
*/
protected canBorrow(...args: unknown[]): boolean {
const hash = this.hashFn(...args);
return !(!this.borrowedResourceStore.has(hash) && this.canTake(...args) === 0);
}
/**
* Creates a resource and stores it in the pool.
* The method takes arguments that will be provided to a resource factory.
*
* @param [args]
*/
protected createResource(...args: unknown[]): Resource<T> {
const
hash = this.hashFn(...args);
if (this.maxSize <= this.size) {
throw new Error('The pool contains too many resources');
}
let
store = this.resourceStore.get(hash);
if (store == null) {
store = [];
this.resourceStore.set(hash, store);
}
const
resource = <Resource<T>>this.resourceFactory(...args);
Object.defineProperty(resource, hashVal, {
configurable: true,
writable: true,
value: hash
});
Object.defineProperty(resource, borrowCounter, {
configurable: true,
writable: true,
value: 0
});
store.push(resource);
this.availableResources.add(resource);
return resource;
}
/**
* Wraps the specified resource and returns the wrapper
* @param resource
*/
protected wrapResource(resource: Resource<T> | null): OptionalWrappedResource<T> {
let
released = false;
return {
value: resource,
free: (...args) => {
if (released) {
return;
}
released = true;
if (resource != null) {
this.free(resource, ...args);
}
},
destroy: () => {
if (released) {
return;
}
released = true;
if (resource != null) {
const
{onFree} = this;
this.onFree = undefined;
this.free(resource);
this.availableResources.delete(resource);
this.onFree = onFree;
if (resource[borrowCounter] === 0) {
this.resourceStore.get(resource[hashVal])?.pop();
this.resourceDestructor?.(resource);
}
}
}
};
}
/**
* Releases the specified resource.
* The method takes arguments that will be provided to hook handlers.
*
* @param resource
* @param [args]
*/
protected free(resource: Resource<T>, ...args: unknown[]): void {
resource[borrowCounter]--;
this.unavailableResources.delete(resource);
this.availableResources.add(resource);
if (
resource[borrowCounter] === 0 &&
this.borrowedResourceStore.get(resource[hashVal]) === resource
) {
this.borrowedResourceStore.delete(resource[hashVal]);
}
if (resource[borrowCounter] === 0) {
this.resourceStore.get(resource[hashVal])?.push(resource);
}
const
event = this.events.get(resource[hashVal])?.pop();
if (event != null) {
this.emitter.emit(event);
}
if (this.onFree != null) {
this.onFree(resource, this, ...args);
}
}
}