Skip to content

Commit

Permalink
chore: [#1451] Starts on implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed May 29, 2024
1 parent fa03438 commit bacf3cc
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 30 deletions.
82 changes: 54 additions & 28 deletions packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export default class AsyncTaskManager {
* @param timerID Timer ID.
*/
public startTimer(timerID: NodeJS.Timeout): void {
if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}
this.runningTimers.push(timerID);
}

Expand All @@ -58,12 +62,16 @@ export default class AsyncTaskManager {
* @param timerID Timer ID.
*/
public endTimer(timerID: NodeJS.Timeout): void {
if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}
const index = this.runningTimers.indexOf(timerID);
if (index !== -1) {
this.runningTimers.splice(index, 1);
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
}
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
}

Expand All @@ -73,6 +81,10 @@ export default class AsyncTaskManager {
* @param immediateID Immediate ID.
*/
public startImmediate(immediateID: NodeJS.Immediate): void {
if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}
this.runningImmediates.push(immediateID);
}

Expand All @@ -82,12 +94,16 @@ export default class AsyncTaskManager {
* @param immediateID Immediate ID.
*/
public endImmediate(immediateID: NodeJS.Immediate): void {
if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}
const index = this.runningImmediates.indexOf(immediateID);
if (index !== -1) {
this.runningImmediates.splice(index, 1);
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
}
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
}

Expand All @@ -98,6 +114,10 @@ export default class AsyncTaskManager {
* @returns Task ID.
*/
public startTask(abortHandler?: () => void): number {
if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}
const taskID = this.newTaskID();
this.runningTasks[taskID] = abortHandler ? abortHandler : () => {};
this.runningTaskCount++;
Expand All @@ -110,27 +130,16 @@ export default class AsyncTaskManager {
* @param taskID Task ID.
*/
public endTask(taskID: number): void {
if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}
if (this.runningTasks[taskID]) {
delete this.runningTasks[taskID];
this.runningTaskCount--;
if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
}
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
// In some cases, microtasks are used by transformed code and waitUntilComplete() is then resolved too early.
// To cater for this we use setTimeout() which has the lowest priority and will be executed last.
// "10ms" is an arbitrary value, but it seem to be enough when performing many manual tests.
this.waitUntilCompleteTimer = TIMER.setTimeout(() => {
this.waitUntilCompleteTimer = null;
if (
!this.runningTaskCount &&
!this.runningTimers.length &&
!this.runningImmediates.length
) {
this.resolveWhenComplete();
}
}, 10);
}
}
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.resolveWhenComplete();
}
}

Expand All @@ -157,11 +166,28 @@ export default class AsyncTaskManager {
* Resolves when complete.
*/
private resolveWhenComplete(): void {
const resolvers = this.waitUntilCompleteResolvers;
this.waitUntilCompleteResolvers = [];
for (const resolver of resolvers) {
resolver();
if (this.runningTaskCount || this.runningTimers.length || this.runningImmediates.length) {
return;
}

if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}

// In some cases, microtasks are used by transformed code and waitUntilComplete() is then resolved too early.
// To cater for this we use setTimeout() which has the lowest priority and will be executed last.
// "10ms" is an arbitrary value, but it seem to be enough when performing many manual tests.
this.waitUntilCompleteTimer = TIMER.setTimeout(() => {
this.waitUntilCompleteTimer = null;
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
const resolvers = this.waitUntilCompleteResolvers;
this.waitUntilCompleteResolvers = [];
for (const resolver of resolvers) {
resolver();
}
}
}, 10);
}

/**
Expand Down
12 changes: 12 additions & 0 deletions packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export default class BrowserFrameFactory {
}

if (!frame.childFrames.length) {
if (frame.window && frame.window[PropertySymbol.mutationObservers]) {
for (const mutationObserver of frame.window[PropertySymbol.mutationObservers]) {
mutationObserver.disconnect();
}
frame.window[PropertySymbol.mutationObservers] = [];
}
return frame[PropertySymbol.asyncTaskManager]
.destroy()
.then(() => {
Expand All @@ -60,6 +66,12 @@ export default class BrowserFrameFactory {

Promise.all(frame.childFrames.slice().map((childFrame) => this.destroyFrame(childFrame)))
.then(() => {
if (frame.window && frame.window[PropertySymbol.mutationObservers]) {
for (const mutationObserver of frame.window[PropertySymbol.mutationObservers]) {
mutationObserver.disconnect();
}
frame.window[PropertySymbol.mutationObservers] = [];
}
return frame[PropertySymbol.asyncTaskManager].destroy().then(() => {
frame[PropertySymbol.exceptionObserver]?.disconnect();
if (frame.window) {
Expand Down
4 changes: 3 additions & 1 deletion packages/happy-dom/src/window/BrowserWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
// Used for tracking capture event listeners to improve performance when they are not used.
// See EventTarget class.
public [PropertySymbol.captureEventListenerCount]: { [eventType: string]: number } = {};
public readonly [PropertySymbol.mutationObservers]: MutationObserver[] = [];
public [PropertySymbol.mutationObservers]: MutationObserver[] = [];
public readonly [PropertySymbol.readyStateManager] = new DocumentReadyStateManager(this);
public [PropertySymbol.asyncTaskManager]: AsyncTaskManager | null = null;
public [PropertySymbol.location]: Location;
Expand Down Expand Up @@ -1319,6 +1319,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal
mutationObserver.disconnect();
}

this[PropertySymbol.mutationObservers] = [];

// Disconnects nodes from the document, so that they can be garbage collected.
for (const node of this.document[PropertySymbol.childNodes].slice()) {
// Makes sure that something won't be triggered by the disconnect.
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/test/nodes/document/Document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1255,7 +1255,7 @@ describe('Document', () => {
expect((<Event>readyChangeEvent).target).toBe(document);
expect(document.readyState).toBe(DocumentReadyStateEnum.complete);
resolve(null);
}, 1);
}, 20);
});
});

Expand Down

0 comments on commit bacf3cc

Please sign in to comment.