@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
90 lines (81 loc) • 2.85 kB
text/typescript
export interface WeakTickerTarget {
onTick(tickNo: number, tickMs: number): "keep" | "unschedule";
}
/**
* A perf efficient approximate scheduler which doesn't retain the scheduled
* objects in memory, so they remain subject for GC.
*/
export class WeakTicker {
private slots = new Map<
number,
{
refs: Set<WeakRef<WeakTickerTarget>>;
nextTickNos: WeakMap<WeakTickerTarget, number>;
interval: NodeJS.Timeout;
}
>();
/**
* Schedules a call to target.onTick() to be called periodically, every tickMs
* approximately.
* - The target scheduled will NOT be retained from GC. This is the main
* difference with setInterval() and the reason why we accept an object, not
* a closure.
* - The 1st call to target.onTick() will happen between 0 and tickMs from
* now: this is the second difference from setInterval(). Then, next calls
* will follow. The current tick number is passed to onTick().
* - If the same target is scheduled again, its tick number will be reset to
* 0, as if it's scheduled the very 1st time. The 2nd scheduling is cheap
* (just 2 map lookups), so an object can be rescheduled-over as many times
* as needed.
* - If target.onTick() returns "unschedule", the target will be unscheduled.
*/
schedule(target: WeakTickerTarget, tickMs: number): void {
// We DO NOT use any closures here! Otherwise, target would be retained in
// that closures, and it won't be garbage collected.
let slot = this.slots.get(tickMs);
if (!slot) {
slot = {
refs: new Set(),
nextTickNos: new WeakMap(),
interval: setInterval(this.onTick.bind(this, tickMs), tickMs).unref(),
};
this.slots.set(tickMs, slot);
}
if (!slot.nextTickNos.has(target)) {
const ref = new WeakRef(target);
slot.refs.add(ref);
}
slot.nextTickNos.set(target, 0);
}
/**
* Returns true if there are no targets scheduled at the moment.
*/
isEmpty(): boolean {
return this.slots.size === 0;
}
/**
* Called by internal setInterval().
*/
private onTick(tickMs: number): void {
const slot = this.slots.get(tickMs)!;
for (const ref of slot.refs) {
const target = ref.deref();
if (target) {
const nextTickNo = slot.nextTickNos.get(target)!;
if (target.onTick(nextTickNo, tickMs) === "unschedule") {
slot.refs.delete(ref);
slot.nextTickNos.delete(target);
}
slot.nextTickNos.set(target, nextTickNo + 1);
} else {
slot.refs.delete(ref);
// Target was garbage collected, which means it is already auto-removed
// from slot.nextTickNos WeakMap too.
}
}
if (slot.refs.size === 0) {
clearInterval(slot.interval);
this.slots.delete(tickMs);
}
}
}