fix(domtools): Prevent race conditions during DomTools initialization and improve runOnce error handling

This commit is contained in:
2025-11-16 15:29:23 +00:00
parent d16bc1652d
commit 039ee19ffd
4 changed files with 508 additions and 90 deletions

View File

@@ -18,32 +18,54 @@ export class DomTools {
// ======
// STATIC
// ======
private static initializationPromise: Promise<DomTools> | null = null;
/**
* setups domtools
*/
public static async setupDomTools(optionsArg: IDomToolsContructorOptions = {}) {
let domToolsInstance: DomTools;
if (!globalThis.deesDomTools && !optionsArg.ignoreGlobal) {
globalThis.deesDomTools = new DomTools(optionsArg);
domToolsInstance = globalThis.deesDomTools;
// lets make sure the dom is ready
const readyStateChangedFunc = () => {
if (document.readyState === 'interactive' || document.readyState === 'complete') {
domToolsInstance.elements.headElement = document.querySelector('head');
domToolsInstance.elements.bodyElement = document.querySelector('body');
domToolsInstance.domReady.resolve();
}
};
document.addEventListener('readystatechange', readyStateChangedFunc);
domToolsInstance.domToolsReady.resolve();
} else if (optionsArg.ignoreGlobal) {
domToolsInstance = new DomTools(optionsArg);
} else {
domToolsInstance = globalThis.deesDomTools;
public static async setupDomTools(optionsArg: IDomToolsContructorOptions = {}): Promise<DomTools> {
// If initialization is already in progress and we're not ignoring global, wait for it
if (!optionsArg.ignoreGlobal && DomTools.initializationPromise) {
return await DomTools.initializationPromise;
}
// Create initialization promise to prevent race conditions
if (!optionsArg.ignoreGlobal) {
DomTools.initializationPromise = (async () => {
let domToolsInstance: DomTools;
if (!globalThis.deesDomTools) {
globalThis.deesDomTools = new DomTools(optionsArg);
domToolsInstance = globalThis.deesDomTools;
// lets make sure the dom is ready
const readyStateChangedFunc = () => {
if (document.readyState === 'interactive' || document.readyState === 'complete') {
domToolsInstance.elements.headElement = document.querySelector('head');
domToolsInstance.elements.bodyElement = document.querySelector('body');
// Initialize keyboard now that document.body exists
domToolsInstance.keyboard = new Keyboard(document.body);
domToolsInstance.domReady.resolve();
}
};
// Check current state immediately to avoid race condition
if (document.readyState === 'interactive' || document.readyState === 'complete') {
readyStateChangedFunc();
} else {
document.addEventListener('readystatechange', readyStateChangedFunc);
}
domToolsInstance.domToolsReady.resolve();
} else {
domToolsInstance = globalThis.deesDomTools;
}
await domToolsInstance.domToolsReady.promise;
return domToolsInstance;
})();
return await DomTools.initializationPromise;
} else {
// ignoreGlobal case - create isolated instance
const domToolsInstance = new DomTools(optionsArg);
return domToolsInstance;
}
await domToolsInstance.domToolsReady.promise;
return domToolsInstance;
}
/**
@@ -95,7 +117,7 @@ export class DomTools {
public deesComms = new plugins.deesComms.DeesComms();
public scroller = new Scroller(this);
public themeManager = new ThemeManager(this);
public keyboard = new Keyboard(document.body);
public keyboard: Keyboard = null; // Initialized after DOM ready to avoid accessing document.body before it exists
public domToolsReady = plugins.smartpromise.defer();
public domReady = plugins.smartpromise.defer();
@@ -105,6 +127,7 @@ export class DomTools {
private runOnceTrackerStringMap = new plugins.lik.Stringmap();
private runOnceResultMap = new plugins.lik.FastMap();
private runOnceErrorMap = new plugins.lik.FastMap();
/**
* run a function once and always get the Promise of the first execution
@@ -116,15 +139,27 @@ export class DomTools {
if (!this.runOnceTrackerStringMap.checkString(identifierArg)) {
this.runOnceTrackerStringMap.addString(identifierArg);
this.runOnceTrackerStringMap.addString(runningId);
const result = await funcArg();
this.runOnceResultMap.addToMap(identifierArg, result);
this.runOnceTrackerStringMap.removeString(runningId);
try {
const result = await funcArg();
this.runOnceResultMap.addToMap(identifierArg, result);
} catch (error) {
// Store error so waiting callers can receive it
this.runOnceErrorMap.addToMap(identifierArg, error);
} finally {
// Always remove running flag to prevent permanent stuck state
this.runOnceTrackerStringMap.removeString(runningId);
}
}
return await this.runOnceTrackerStringMap.registerUntilTrue(
(stringMap) => {
return !stringMap.includes(runningId);
},
() => {
// Check if there was an error and re-throw it
const error = this.runOnceErrorMap.getByKey(identifierArg);
if (error) {
throw error;
}
return this.runOnceResultMap.getByKey(identifierArg);
}
);