2021-04-29 23:02:27 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
/* eslint-disable max-classes-per-file */
|
|
|
|
/* eslint-disable no-await-in-loop */
|
|
|
|
|
|
|
|
import EventEmitter, { once } from 'events';
|
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { JobQueueStore, StoredJob } from '../../jobs/types';
|
2021-04-29 23:02:27 +00:00
|
|
|
import { sleep } from '../../util/sleep';
|
2022-12-21 18:41:48 +00:00
|
|
|
import { drop } from '../../util/drop';
|
2021-04-29 23:02:27 +00:00
|
|
|
|
|
|
|
export class TestJobQueueStore implements JobQueueStore {
|
|
|
|
events = new EventEmitter();
|
|
|
|
|
|
|
|
private openStreams = new Set<string>();
|
|
|
|
|
|
|
|
private pipes = new Map<string, Pipe>();
|
|
|
|
|
|
|
|
storedJobs: Array<StoredJob> = [];
|
|
|
|
|
|
|
|
constructor(jobs: ReadonlyArray<StoredJob> = []) {
|
|
|
|
jobs.forEach(job => {
|
2022-12-21 18:41:48 +00:00
|
|
|
drop(this.insert(job));
|
2021-04-29 23:02:27 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-10-29 23:19:44 +00:00
|
|
|
async insert(
|
|
|
|
job: Readonly<StoredJob>,
|
|
|
|
{ shouldPersist = true }: Readonly<{ shouldPersist?: boolean }> = {}
|
|
|
|
): Promise<void> {
|
2021-04-29 23:02:27 +00:00
|
|
|
await fakeDelay();
|
|
|
|
|
|
|
|
this.storedJobs.forEach(storedJob => {
|
|
|
|
if (job.id === storedJob.id) {
|
|
|
|
throw new Error('Cannot store two jobs with the same ID');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-10-29 23:19:44 +00:00
|
|
|
if (shouldPersist) {
|
|
|
|
this.storedJobs.push(job);
|
|
|
|
}
|
2021-04-29 23:02:27 +00:00
|
|
|
|
|
|
|
this.getPipe(job.queueType).add(job);
|
|
|
|
|
|
|
|
this.events.emit('insert');
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete(id: string): Promise<void> {
|
|
|
|
await fakeDelay();
|
|
|
|
|
|
|
|
this.storedJobs = this.storedJobs.filter(job => job.id !== id);
|
|
|
|
|
|
|
|
this.events.emit('delete');
|
|
|
|
}
|
|
|
|
|
|
|
|
stream(queueType: string): Pipe {
|
|
|
|
if (this.openStreams.has(queueType)) {
|
|
|
|
throw new Error('Cannot stream the same queueType more than once');
|
|
|
|
}
|
|
|
|
this.openStreams.add(queueType);
|
|
|
|
|
|
|
|
return this.getPipe(queueType);
|
|
|
|
}
|
|
|
|
|
|
|
|
pauseStream(queueType: string): void {
|
|
|
|
return this.getPipe(queueType).pause();
|
|
|
|
}
|
|
|
|
|
|
|
|
resumeStream(queueType: string): void {
|
|
|
|
return this.getPipe(queueType).resume();
|
|
|
|
}
|
|
|
|
|
|
|
|
private getPipe(queueType: string): Pipe {
|
|
|
|
const existingPipe = this.pipes.get(queueType);
|
|
|
|
if (existingPipe) {
|
|
|
|
return existingPipe;
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = new Pipe();
|
|
|
|
this.pipes.set(queueType, result);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Pipe implements AsyncIterable<StoredJob> {
|
|
|
|
private queue: Array<StoredJob> = [];
|
|
|
|
|
|
|
|
private eventEmitter = new EventEmitter();
|
|
|
|
|
|
|
|
private isLocked = false;
|
|
|
|
|
|
|
|
private isPaused = false;
|
|
|
|
|
|
|
|
add(value: Readonly<StoredJob>) {
|
|
|
|
this.queue.push(value);
|
|
|
|
this.eventEmitter.emit('add');
|
|
|
|
}
|
|
|
|
|
|
|
|
async *[Symbol.asyncIterator]() {
|
|
|
|
if (this.isLocked) {
|
|
|
|
throw new Error('Cannot iterate over a pipe more than once');
|
|
|
|
}
|
|
|
|
this.isLocked = true;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
for (const value of this.queue) {
|
|
|
|
await this.waitForUnpaused();
|
|
|
|
yield value;
|
|
|
|
}
|
|
|
|
this.queue = [];
|
|
|
|
|
|
|
|
// We do this because we want to yield values in series.
|
|
|
|
await once(this.eventEmitter, 'add');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pause(): void {
|
|
|
|
this.isPaused = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
resume(): void {
|
|
|
|
this.isPaused = false;
|
|
|
|
this.eventEmitter.emit('resume');
|
|
|
|
}
|
|
|
|
|
|
|
|
private async waitForUnpaused() {
|
|
|
|
if (this.isPaused) {
|
|
|
|
await once(this.eventEmitter, 'resume');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function fakeDelay(): Promise<void> {
|
|
|
|
return sleep(0);
|
|
|
|
}
|