@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
137 lines • 5.67 kB
JavaScript
import { notEmpty } from '@stryker-mutator/util';
import { BehaviorSubject, filter, ignoreElements, lastValueFrom, mergeMap, ReplaySubject, Subject, takeUntil, tap, zip, } from 'rxjs';
import { tokens } from 'typed-inject';
import { coreTokens } from '../di/index.js';
const MAX_CONCURRENT_INIT = 2;
createTestRunnerPool.inject = tokens(coreTokens.testRunnerFactory, coreTokens.testRunnerConcurrencyTokens);
export function createTestRunnerPool(factory, concurrencyToken$) {
return new Pool(factory, concurrencyToken$);
}
createCheckerPool.inject = tokens(coreTokens.checkerFactory, coreTokens.checkerConcurrencyTokens);
export function createCheckerPool(factory, concurrencyToken$) {
return new Pool(factory, concurrencyToken$);
}
/**
* Represents a work item: an input with a task and with a `result$` observable where the result (exactly one) will be streamed to.
*/
class WorkItem {
input;
task;
resultSubject = new Subject();
result$ = this.resultSubject.asObservable();
/**
* @param input The input to the ask
* @param task The task, where a resource and input is presented
*/
constructor(input, task) {
this.input = input;
this.task = task;
}
async execute(resource) {
try {
const output = await this.task(resource, this.input);
this.resultSubject.next(output);
this.resultSubject.complete();
}
catch (err) {
this.resultSubject.error(err);
}
}
reject(error) {
this.resultSubject.error(error);
}
complete() {
this.resultSubject.complete();
}
}
/**
* Represents a pool of resources. Use `schedule` to schedule work to be executed on the resources.
* The pool will automatically recycle the resources, but will make sure only one task is executed
* on one resource at any one time. Creates as many resources as the concurrency tokens allow.
* Also takes care of the initialing of the resources (with `init()`)
*/
export class Pool {
// The init subject. Using an RxJS subject instead of a promise, so errors are silently ignored when nobody is listening
initSubject = new ReplaySubject();
// The disposedSubject emits true when it is disposed, and false when not disposed yet
disposedSubject = new BehaviorSubject(false);
// The dispose$ only emits one `true` value when disposed (never emits `false`). Useful for `takeUntil`
dispose$ = this.disposedSubject.pipe(filter((isDisposed) => isDisposed));
createdResources = [];
// The queued work items. This is a replay subject, so scheduled work items can easily be rejected after it was picked up
todoSubject = new ReplaySubject();
constructor(factory, concurrencyToken$) {
// Stream resources that are ready to pick up work
const resourcesSubject = new Subject();
// Stream ongoing work.
zip(resourcesSubject, this.todoSubject)
.pipe(mergeMap(async ([resource, workItem]) => {
await workItem.execute(resource);
resourcesSubject.next(resource); // recycle resource so it can pick up more work
}), ignoreElements(), takeUntil(this.dispose$))
.subscribe({
error: (error) => {
this.todoSubject.subscribe((workItem) => workItem.reject(error));
},
});
// Create resources
concurrencyToken$
.pipe(takeUntil(this.dispose$), mergeMap(async () => {
if (this.disposedSubject.value) {
// Don't create new resources when disposed
return;
}
const resource = factory();
this.createdResources.push(resource);
await resource.init?.();
return resource;
}, MAX_CONCURRENT_INIT), filter(notEmpty), tap({
complete: () => {
// Signal init complete
this.initSubject.next();
this.initSubject.complete();
},
error: (err) => {
this.initSubject.error(err);
},
}))
.subscribe({
next: (resource) => resourcesSubject.next(resource),
error: (err) => resourcesSubject.error(err),
});
}
/**
* Returns a promise that resolves if all concurrency tokens have resulted in initialized resources.
* This is optional, resources will get initialized either way.
*/
async init() {
await lastValueFrom(this.initSubject);
}
/**
* Schedules a task to be executed on resources in the pool. Each input is paired with a resource, which allows async work to be done.
* @param input$ The inputs to pair up with a resource.
* @param task The task to execute on each resource
*/
schedule(input$, task) {
return input$.pipe(mergeMap((input) => {
const workItem = new WorkItem(input, task);
this.todoSubject.next(workItem);
return workItem.result$;
}));
}
/**
* Dispose the pool
*/
async dispose() {
if (!this.disposedSubject.value) {
this.disposedSubject.next(true);
this.todoSubject.subscribe((workItem) => workItem.complete());
this.todoSubject.complete();
await Promise.all(
// We're mixing promises with undefined values, which triggers the lint warning. We can safely ignore it here.
// eslint-disable-next-line @typescript-eslint/await-thenable
this.createdResources.map((resource) => resource.dispose?.()));
}
}
}
//# sourceMappingURL=pool.js.map