69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/**
|
|
* This class tries to enforce a state machine that looks something like this:
|
|
*
|
|
* .--------------------. called .-----------. called .---------------------.
|
|
* | | --------> | | -------> | |
|
|
* | Nothing is running | | 1 running | | 1 running, 1 queued |
|
|
* | | <-------- | | <------- | |
|
|
* '--------------------' done '-----------' done '---------------------'
|
|
* | ^
|
|
* '-----------'
|
|
* called
|
|
*
|
|
* Most notably, if something is queued and the function is called again, we discard the
|
|
* previously queued task completely.
|
|
*/
|
|
export class LatestQueue {
|
|
private isRunning: boolean;
|
|
|
|
private queuedTask?: () => Promise<void>;
|
|
|
|
private onceEmptyCallbacks: Array<() => unknown>;
|
|
|
|
constructor() {
|
|
this.isRunning = false;
|
|
this.onceEmptyCallbacks = [];
|
|
}
|
|
|
|
/**
|
|
* Does one of the following:
|
|
*
|
|
* 1. Runs the task immediately.
|
|
* 2. Enqueues the task, destroying any previously-enqueued task. In other words, 0 or 1
|
|
* tasks will be enqueued at a time.
|
|
*/
|
|
add(task: () => Promise<void>): void {
|
|
if (this.isRunning) {
|
|
this.queuedTask = task;
|
|
} else {
|
|
this.isRunning = true;
|
|
task().finally(() => {
|
|
this.isRunning = false;
|
|
|
|
const { queuedTask } = this;
|
|
if (queuedTask) {
|
|
this.queuedTask = undefined;
|
|
this.add(queuedTask);
|
|
} else {
|
|
try {
|
|
this.onceEmptyCallbacks.forEach(callback => {
|
|
callback();
|
|
});
|
|
} finally {
|
|
this.onceEmptyCallbacks = [];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a callback to be called the first time the queue goes from "running" to "empty".
|
|
*/
|
|
onceEmpty(callback: () => unknown): void {
|
|
this.onceEmptyCallbacks.push(callback);
|
|
}
|
|
}
|