Files
workavia-drive/tdrive/connectors/onlyoffice-connector/src/lib/pending-request-matcher.ts
T
2024-09-16 17:32:34 +02:00

107 lines
3.9 KiB
TypeScript

import logger from './logger';
export type PendingRequestCallback<TResult> = (timeout: boolean, result?: TResult) => Promise<void>;
/** Represents a single pending query that can be resolved with success or failure */
class PendingRequest<TResult> {
private readonly startedAt = new Date();
private callbacks?: PendingRequestCallback<TResult>[] = [];
constructor(public readonly key: string, public readonly userdata: string, callback: PendingRequestCallback<TResult>) {
this.addCallback(callback);
}
public addCallback(callback: PendingRequestCallback<TResult>) {
if (!this.callbacks) throw new Error("Cannot add callback to PendingRequest after it's been resolved");
this.callbacks.push(callback);
return this;
}
public getAge() {
return new Date().getTime() - this.startedAt.getTime();
}
public matches(key: string, userdata: string) {
return this.key === key && this.userdata === userdata;
}
public async resolve(...cbArgs: Parameters<PendingRequestCallback<TResult>>) {
if (!this.callbacks) throw new Error("Cannot resolve PendingRequest after it's already been resolved");
const callbacks = this.callbacks;
this.callbacks = null;
return Promise.all(callbacks.map(fn => fn(...cbArgs)));
}
}
/**
* Tracks a list of pending requests that are started with a pair
* of `key` and `userdata` strings. (Note. `userdata` is not the
* typical use of user data as a `void *`, but instead used to
* identify the specific request asynchroneously).
* Both must match.
*
* There is no timing garantee as to when expired requests' callbacks
* are executed.
*/
export class PendingRequestQueue<TResult> {
private queue: PendingRequest<TResult>[] = [];
constructor(private readonly timeoutMs: number, niquystSamplingRatio = 4) {
setInterval(() => {
this.flush();
}, timeoutMs / niquystSamplingRatio);
}
/**
* Add a callback to be called when `gotResult` or `cancelPending` are
* called with identical `key` and `userdata`
* @param key First half of identifying string
* @param userdata Second half of identifying string
* @param callback When the request is resolved, succesfully or in error
*/
public enqueue(key: string, userdata: string, callback: PendingRequestCallback<TResult>) {
const existing = this.queue.find(pending => pending.key === key && pending.userdata === userdata);
if (existing) return void existing.addCallback(callback);
this.queue.push(new PendingRequest(key, userdata, callback));
}
private remove(predicate: (request: PendingRequest<TResult>) => boolean): PendingRequest<TResult>[] {
// Warning: This operation must be synchroneous
const foundRequests = [];
this.queue = this.queue.filter(pending => {
if (!predicate(pending)) return true;
foundRequests.push(pending);
return false;
});
return foundRequests;
}
private async resolve(predicate: (request: PendingRequest<TResult>) => boolean, ...cbArgs: Parameters<PendingRequestCallback<TResult>>) {
const foundRequests = this.remove(predicate);
if (!foundRequests.length) return null;
return Promise.all(foundRequests.map(request => request.resolve(...cbArgs)));
}
protected async flush() {
const expiredRequests = this.remove(request => request.getAge() > this.timeoutMs);
return Promise.all(expiredRequests.map(request => request.resolve(true)));
}
public async gotResult(key: string, userdata: string, result?: TResult) {
const resolutions = this.resolve(request => request.matches(key, userdata), false, result);
if (!resolutions)
logger.error(`Got resolution on pending request that was unknown`, {
key,
userdata,
result,
});
await this.flush();
return resolutions;
}
/** Equivalent to `gotResult` with an `undefined` result. (Callbacks are called) */
public async cancelPending(key: string, userdata: string) {
return this.gotResult(key, userdata, undefined);
}
}