UNPKG

@v4fire/core

Version:
548 lines (445 loc) 13.2 kB
/*! * 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); } } }