@gravity-ui/graph
Version:
Modern graph editor component
136 lines (135 loc) • 4.77 kB
JavaScript
const rAF = typeof window !== "undefined" ? window.requestAnimationFrame : (fn) => global.setTimeout(fn, 16);
const cAF = typeof window !== "undefined" ? window.cancelAnimationFrame : global.clearTimeout;
const getNow = typeof window !== "undefined" ? window.performance.now.bind(window.performance) : global.Date.now.bind(global.Date);
export var ESchedulerPriority;
(function (ESchedulerPriority) {
ESchedulerPriority[ESchedulerPriority["HIGHEST"] = 0] = "HIGHEST";
ESchedulerPriority[ESchedulerPriority["HIGH"] = 1] = "HIGH";
ESchedulerPriority[ESchedulerPriority["MEDIUM"] = 2] = "MEDIUM";
ESchedulerPriority[ESchedulerPriority["LOW"] = 3] = "LOW";
ESchedulerPriority[ESchedulerPriority["LOWEST"] = 4] = "LOWEST";
})(ESchedulerPriority || (ESchedulerPriority = {}));
export class GlobalScheduler {
constructor() {
this.toRemove = [];
this.visibilityChangeHandler = null;
this.tick = this.tick.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
this.schedulers = [[], [], [], [], []];
this.setupVisibilityListener();
}
/**
* Setup listener for page visibility changes.
* When tab becomes visible after being hidden, force immediate update.
* This fixes the issue where tabs opened in background don't render HTML until interaction.
*/
setupVisibilityListener() {
if (typeof document === "undefined") {
return; // Not in browser environment
}
this.visibilityChangeHandler = this.handleVisibilityChange;
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
}
/**
* Handle page visibility changes.
* When page becomes visible, perform immediate update if scheduler is running.
*/
handleVisibilityChange() {
// Only update if page becomes visible and scheduler is running
if (!document.hidden && this._cAFID) {
// Perform immediate update when tab becomes visible
this.performUpdate();
}
}
/**
* Cleanup visibility listener
*/
cleanupVisibilityListener() {
if (this.visibilityChangeHandler && typeof document !== "undefined") {
document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
this.visibilityChangeHandler = null;
}
}
getSchedulers() {
return this.schedulers;
}
addScheduler(scheduler, index = ESchedulerPriority.MEDIUM) {
this.schedulers[index].push(scheduler);
return () => this.removeScheduler(scheduler, index);
}
removeScheduler(scheduler, index = ESchedulerPriority.MEDIUM) {
this.toRemove.push([scheduler, index]);
}
start() {
if (!this._cAFID) {
this._cAFID = rAF(this.tick);
}
}
stop() {
cAF(this._cAFID);
this._cAFID = undefined;
}
/**
* Cleanup method to be called when GlobalScheduler is no longer needed.
* Stops the scheduler and removes event listeners.
*/
destroy() {
this.stop();
this.cleanupVisibilityListener();
}
tick() {
this.performUpdate();
this._cAFID = rAF(this.tick);
}
performUpdate() {
const startTime = getNow();
let schedulers = [];
for (let i = 0; i < this.schedulers.length; i += 1) {
schedulers = this.schedulers[i];
for (let j = 0; j < schedulers.length; j += 1) {
schedulers[j].performUpdate(getNow() - startTime);
}
}
// Process deferred removals after all schedulers have been executed
for (const [scheduler, index] of this.toRemove) {
const schedulerIndex = this.schedulers[index].indexOf(scheduler);
if (schedulerIndex !== -1) {
this.schedulers[index].splice(schedulerIndex, 1);
}
}
this.toRemove.length = 0;
}
}
export const globalScheduler = new GlobalScheduler();
export const scheduler = globalScheduler;
export class Scheduler {
constructor() {
this.performUpdate = this.performUpdate.bind(this);
this.sheduled = false;
globalScheduler.addScheduler(this);
}
setRoot(root) {
this.root = root;
}
start() {
globalScheduler.addScheduler(this);
}
stop() {
globalScheduler.removeScheduler(this);
}
update() {
this.root?.traverseDown(this.iterator);
}
iterator(node) {
return node.data.iterate();
}
scheduleUpdate() {
this.sheduled = true;
}
performUpdate() {
if (this.sheduled) {
this.sheduled = false;
this.update();
}
}
}