Skip to content

Commit

Permalink
feat: Switch to single queue (#3239)
Browse files Browse the repository at this point in the history
## Description

1. What is this PR about (link the issue and add a short description)

## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env.example`
and the `builder/env-check.js` if mandatory
  • Loading branch information
istarkov authored Apr 24, 2024
1 parent dd1657c commit 1b3032d
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 11 deletions.
72 changes: 72 additions & 0 deletions packages/sdk-components-react/src/html-embed-patchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
const isDOMContentLoaded = () => {
return (
document.readyState === "complete" || document.readyState === "interactive"
);
};

const eventListenerTasks: Array<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
() => void
> = [];

let domContentLoadedPatched = false;

export const patchDomEvents = () => {
// If original event is not fired yet, do nothing as it can cause serious side effects.
if (isDOMContentLoaded() === false) {
console.error("DOMContentLoaded event has not been fired yet");
return;
}

if (domContentLoadedPatched) {
return;
}

domContentLoadedPatched = true;

console.info("Patching DOMContentLoaded event listener");

const originalAddEventListener = document.addEventListener;
const originalWindowAddEventListener = window.addEventListener;

const domContentLoadedEvent = new Event("DOMContentLoaded");
const windowLoadEvent = new Event("load");

window.addEventListener = (type: any, listener: any, options?: any) => {
if (type === "DOMContentLoaded") {
eventListenerTasks.push(() =>
listener.call(window, domContentLoadedEvent)
);
// We do not call original event listeners as everything is already loaded and orinal event is not going to be fired.
} else if (type === "load") {
// We store the listener to execute it later
eventListenerTasks.push(() => listener.call(window, windowLoadEvent));
originalWindowAddEventListener.call(window, type, listener, options);
} else {
// For all other events, use the original method

originalWindowAddEventListener.call(window, type, listener, options);
}
};

document.addEventListener = (type: any, listener: any, options: any) => {
if (type === "DOMContentLoaded") {
// We store the listener to execute it later
eventListenerTasks.push(() =>
listener.call(document, domContentLoadedEvent)
);
// We do not call original event listeners as everything is already loaded and orinal event is not going to be fired.
} else {
// For all other events, use the original method
originalAddEventListener.call(document, type, listener, options);
}
};
};

export const executeDomEvents = () => {
for (const task of eventListenerTasks) {
task();
}
eventListenerTasks.length = 0;
};
51 changes: 40 additions & 11 deletions packages/sdk-components-react/src/html-embed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import {
} from "react";
import { mergeRefs } from "@react-aria/utils";
import { ReactSdkContext } from "@webstudio-is/react-sdk";
import { executeDomEvents, patchDomEvents } from "./html-embed-patchers";

export const __testing__ = {
scriptTestIdPrefix: "client-",
};

const insertScript = (
sourceScript: HTMLScriptElement
): Promise<HTMLScriptElement> => {
const insertScript = (sourceScript: HTMLScriptElement): Promise<void> => {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const hasSrc = sourceScript.hasAttribute("src");
Expand All @@ -35,7 +34,7 @@ const insertScript = (

if (hasSrc) {
script.addEventListener("load", () => {
resolve(script);
resolve();
});
script.addEventListener("error", reject);
} else {
Expand All @@ -46,21 +45,54 @@ const insertScript = (

// Run the callback immediately for inline scripts.
if (hasSrc === false) {
resolve(script);
resolve();
}
});
};

type ScriptTask = () => Promise<HTMLScriptElement>;
type ScriptTask = () => Promise<void>;

/**
* We want to execute scripts from all embeds sequentially to preserve execution order.
*/
const syncTasksQueue: ScriptTask[] = [];
let processing = false;

const processSyncTasks = async (syncTasks: ScriptTask[]) => {
syncTasksQueue.push(...syncTasks);

// await 1 tick so tasks from all HTMLEmbeds are added to the queue
await Promise.resolve();

if (processing) {
return;
}

patchDomEvents();

console.info("Start processing sync tasks");
processing = true;

while (syncTasksQueue.length > 0) {
const task = syncTasksQueue.shift()!;
await task();
}

executeDomEvents();

processing = false;
console.info("Stop processing sync tasks");
};

// Inspiration https://ghinda.net/article/script-tags
const execute = async (container: HTMLElement) => {
const execute = (container: HTMLElement) => {
const scripts = container.querySelectorAll("script");
const syncTasks: Array<ScriptTask> = [];
const asyncTasks: Array<ScriptTask> = [];

scripts.forEach((script) => {
const tasks = script.hasAttribute("async") ? asyncTasks : syncTasks;

tasks.push(() => {
return insertScript(script);
});
Expand All @@ -71,10 +103,7 @@ const execute = async (container: HTMLElement) => {
task();
}

// Insert the script tags sequentially to preserve execution order.
for (const task of syncTasks) {
await task();
}
processSyncTasks(syncTasks);
};

type ChildProps = {
Expand Down

0 comments on commit 1b3032d

Please sign in to comment.