mqrpc
Version:
💫 Easy RPC over RabbitMQ
156 lines (130 loc) • 4.55 kB
text/typescript
import { newPromiseAndCallbacks } from './promises'
export class TimeoutExpired extends Error {
constructor(public timeout: Timeout) {
super(`${timeout.id} expired after ${timeout.length}ms`)
}
}
export interface Timeout {
id: string
length: number
}
export interface ActiveTimeout extends Timeout {
handle: NodeJS.Timer
}
export interface Entry {
readonly promise: Promise<any>
readonly reject: (err: Error) => void
readonly timeouts: Map<string, ActiveTimeout>
}
/**
* @private
*
* Activates a new timeout and returns it. The given reject will be called
* with a TimeoutExpired error if the timer expires.
*
* @param {Timeout} timeout The timeout to activate
* @param {Function} reject Called with a TimeoutExpired error when the timer expires
*/
const activateTimeout = (timeout: Timeout, reject: (err: Error) => void): ActiveTimeout => {
return Object.assign({}, timeout, {
handle: setTimeout(() => {
reject(new TimeoutExpired(timeout))
}, timeout.length)
})
}
export default class Timer {
protected entries: Map<string, Entry>
constructor() {
this.entries = new Map<string, Entry>()
}
/**
* Adds and starts the given timeouts for the given `entryId`. If an entry
* with the same ID exists, timeouts will be appended to it. Otherwise, a new
* entry is created.
*
* The promise returned will never resolve, but it will reject when one of the
* timeouts expires.
*
* @param {string} entryId An ID under which to add the timeouts.
* @param {Timeout[]} ...timeouts Timeout objects, with an ID and length in ms.
* @return {Promise<void>} Rejects when a timeout expires. Never resolves.
*/
addTimeouts(entryId: string, ...timeouts: Timeout[]): Promise<void> {
const entry = this.ensureEntry(entryId)
timeouts.forEach(timeout => {
if (entry.timeouts.has(timeout.id)) {
throw new Error(`Timeout with ID ${timeout.id} already exists for entry ${entryId}`)
}
entry.timeouts.set(timeout.id, activateTimeout(timeout, entry.reject))
})
return entry.promise
}
/**
* Restarts all timeouts with the given IDs for the given entry.
*
* @param {string} entryId The ID under which the timeouts are.
* @param {string[]} ...timeoutIds The IDs for timeouts to restart.
*/
restartTimeouts(entryId: string, ...timeoutIds: string[]): void {
const entry = this.entries.get(entryId)
if (!entry) throw new Error(`Entry with ID ${entryId} does not exist`)
timeoutIds.forEach(id => {
const timeout = entry.timeouts.get(id)
if (!timeout) throw new Error(`Timeout with ID ${id} does not exist for entry ${entryId}`)
clearTimeout(timeout.handle)
entry.timeouts.set(id, activateTimeout(timeout, entry.reject))
})
}
/**
* Stops and removes the timeouts with the given IDs from the given entry.
*
* @param {string} entryId The ID under which the timeouts are.
* @param {string[]} ...timeoutIds The IDs for timeouts to stop & remove.
*/
removeTimeouts(entryId: string, ...timeoutIds: string[]): void {
const entry = this.entries.get(entryId)
if (!entry) throw new Error(`Entry with ID ${entryId} does not exist`)
timeoutIds.forEach(id => {
const timeout = entry.timeouts.get(id)
if (!timeout) return // idempotent
clearTimeout(timeout.handle)
entry.timeouts.delete(id)
})
}
/**
* Removes an entry. This stops and removes all its timeouts as well.
*
* @param {string} entryId The ID for the entry to remove.
*/
remove(entryId: string): void {
const entry = this.entries.get(entryId)
if (!entry) return // idempotent
entry.timeouts.forEach(timeout => clearTimeout(timeout.handle))
entry.timeouts.clear()
this.entries.delete(entryId)
}
/**
* Removes every entry and every timeout.
*/
clear(): void {
for (const entryId of this.entries.keys()) {
this.remove(entryId)
}
}
/**
* @protected
*
* Returns an existing Entry or creates a new one.
*
* @param {string} entryId The Entry's ID to create or fetch.
* @return {Entry} A new or existing Entry
*/
protected ensureEntry(entryId: string): Entry {
let entry = this.entries.get(entryId)
if (entry) return entry
const [promise, { reject }] = newPromiseAndCallbacks()
entry = { promise, reject, timeouts: new Map<string, ActiveTimeout>() }
this.entries.set(entryId, entry)
return entry
}
}