Skip to content

Commit d88d7ae

Browse files
authored
fix(dom): implement blocking script execution order for HTMLScriptElement (#251)
1 parent c78b3eb commit d88d7ae

File tree

7 files changed

+180
-7
lines changed

7 files changed

+180
-7
lines changed

fixtures/html/simple-module.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.info('call from script-module');

fixtures/html/simple.html

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,15 @@
108108
background-color: rgba(224, 172, 150, 0.594);
109109
padding: 2rem;
110110
}
111+
111112
#float-area>p.first {
112113
background-color: rgba(134, 55, 20, 0.352);
113114
}
114115
</style>
116+
<script src="./simple.js"></script>
115117
<script>
116-
console.log('Hello, world!');
117-
const html = document.firstChild;
118-
console.log(html);
118+
console.info('Simple HTML:', window['foobar']);
119119
</script>
120-
<script src="./simple.js"></script>
121120
</head>
122121

123122
<body>
@@ -132,7 +131,9 @@
132131
</div>
133132
<article>
134133
<p class="primary first">
135-
A range of organizations join the World Wide Web Consortium as Members to work with us to drive the direction of core web technologies and exchange ideas with industry and research leaders. We rotate randomly a few of our Member organizations' logos underneath.
134+
A range of organizations join the World Wide Web Consortium as Members to work with us to drive the direction of
135+
core web technologies and exchange ideas with industry and research leaders. We rotate randomly a few of our
136+
Member organizations' logos underneath.
136137
</p>
137138
<p>
138139
1 October 2024 was W3C's 30th anniversary. We celebrated at our annual TPAC our three decades, advances in the
@@ -152,6 +153,12 @@
152153
<div>Footer</div>
153154
<img class="float-image" src="https://www.gstatic.com/webp/gallery/1.webp" alt="A cute kitten"
154155
style="margin-top: 200px;">
156+
157+
<script>
158+
const header = document.querySelector('#switch');
159+
header.textContent += (' (foobar=' + window['foobar'] + ')');
160+
</script>
161+
<script src="./simple-module.js" type="module"></script>
155162
</body>
156163

157164
</html>

fixtures/html/simple.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,7 @@ run(async () => {
8787
// + 'foobar'
8888
// + '</div>';
8989
});
90+
91+
window['foobar'] = Math.random();
92+
console.info('set global foobar =', window['foobar']);
93+
console.info('script end');
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#include <idgen.hpp>
2+
#include "./browsing_context.hpp"
3+
#include "../html/html_script_element.hpp"
4+
5+
namespace dom
6+
{
7+
uint32_t BrowsingContext::registerScriptForExecution(std::shared_ptr<HTMLScriptElement> script)
8+
{
9+
static TrIdGenerator idgen(0x1);
10+
uint32_t script_id = idgen.get();
11+
script_execution_queue.emplace_back(script_id, std::weak_ptr<HTMLScriptElement>(script));
12+
13+
// Try to execute if this is the first script in the queue
14+
tryExecuteNextScript();
15+
return script_id;
16+
}
17+
18+
void BrowsingContext::tryExecuteNextScript()
19+
{
20+
while (!script_execution_queue.empty())
21+
{
22+
auto &handle = script_execution_queue.front();
23+
auto script = handle.scriptElement.lock();
24+
25+
// If script was destroyed, remove from queue and continue
26+
if (!script)
27+
{
28+
script_execution_queue.pop_front();
29+
continue;
30+
}
31+
32+
// Execute the script and it will return false if not ready yet
33+
if (!script->executeScriptFromQueue())
34+
return; // Script will call notifyScriptExecutionComplete when done
35+
36+
// Script not ready yet, stop trying
37+
break;
38+
}
39+
}
40+
41+
void BrowsingContext::notifyScriptExecutionComplete(uint32_t script_id)
42+
{
43+
if (!script_execution_queue.empty() &&
44+
script_execution_queue.front().scriptId == script_id)
45+
{
46+
script_execution_queue.pop_front();
47+
tryExecuteNextScript();
48+
}
49+
}
50+
}

src/client/dom/browsing_context.hpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <functional>
44
#include <string>
55
#include <memory>
6+
#include <deque>
7+
#include <unordered_map>
68
#include <v8.h>
79

810
#include "./dom_parser.hpp"
@@ -17,6 +19,23 @@ namespace dom
1719
URL,
1820
Source,
1921
};
22+
23+
// Forward declaration to avoid circular dependency
24+
class HTMLScriptElement;
25+
26+
// Script execution handle to decouple from HTMLScriptElement
27+
struct ScriptExecutionHandle
28+
{
29+
uint32_t scriptId;
30+
std::weak_ptr<HTMLScriptElement> scriptElement;
31+
32+
ScriptExecutionHandle(uint32_t id, std::weak_ptr<HTMLScriptElement> element)
33+
: scriptId(id)
34+
, scriptElement(element)
35+
{
36+
}
37+
};
38+
2039
class BrowsingContext : public RuntimeContext
2140
{
2241
public:
@@ -96,7 +115,27 @@ namespace dom
96115
return scriptingContext->updateImportMapFromJSON(json);
97116
}
98117

118+
/**
119+
* Register a script for document-order execution.
120+
* Returns a unique script ID for tracking.
121+
*/
122+
uint32_t registerScriptForExecution(std::shared_ptr<HTMLScriptElement> script);
123+
124+
/**
125+
* Try to execute the next script in the queue if it's ready.
126+
*/
127+
void tryExecuteNextScript();
128+
129+
/**
130+
* Notify that a script has completed execution.
131+
*/
132+
void notifyScriptExecutionComplete(uint32_t script_id);
133+
99134
public:
100135
vector<shared_ptr<Document>> documents;
136+
137+
private:
138+
// Script execution queue using handles to avoid circular dependency
139+
std::deque<ScriptExecutionHandle> script_execution_queue;
101140
};
102141
}

src/client/html/html_script_element.cpp

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ namespace dom
4747
{
4848
compiledScript = browsingContext->createScript(baseURI, isClassicScript() ? SourceTextType::Classic : SourceTextType::ESM);
4949
compiledScript->crossOrigin = crossOrigin == HTMLScriptCrossOrigin::Anonymous ? true : false;
50+
51+
// Register classic scripts (non-async, non-defer) with execution queue
52+
if (isClassicScript() && !async && !defer)
53+
{
54+
usesExecutionQueue = true;
55+
auto scriptElement = dynamic_pointer_cast<HTMLScriptElement>(shared_from_this());
56+
scriptExecutionId = browsingContext->registerScriptForExecution(scriptElement);
57+
}
58+
5059
loadSource();
5160
}
5261
// TODO(yorkie): support "speculationrules"?
@@ -109,7 +118,17 @@ namespace dom
109118
skipScriptExecution = true;
110119

111120
if (!skipScriptExecution)
112-
executeScript();
121+
{
122+
// If script uses execution queue, let the queue manage execution
123+
if (usesExecutionQueue)
124+
{
125+
browsingContext->tryExecuteNextScript();
126+
}
127+
else
128+
{
129+
executeScript();
130+
}
131+
}
113132
}
114133

115134
void HTMLScriptElement::scheduleScriptExecution()
@@ -120,7 +139,18 @@ namespace dom
120139

121140
// Check if the script is already compiled, then schedule the execution by default.
122141
if (scriptCompiled)
123-
executeScript();
142+
{
143+
// If script uses execution queue, let the queue manage execution
144+
if (usesExecutionQueue)
145+
{
146+
auto browsingContext = ownerDocument->lock()->browsingContext;
147+
browsingContext->tryExecuteNextScript();
148+
}
149+
else
150+
{
151+
executeScript();
152+
}
153+
}
124154
}
125155

126156
void HTMLScriptElement::executeScript()
@@ -131,6 +161,29 @@ namespace dom
131161
browsingContext->scriptingContext->evaluate(compiledScript);
132162
scriptExecutedOnce = true;
133163
scriptExecutionScheduled = false;
164+
165+
// Notify browsing context if this script was using the execution queue
166+
if (usesExecutionQueue)
167+
browsingContext->notifyScriptExecutionComplete(scriptExecutionId);
168+
134169
dispatchEvent(dom::DOMEventType::Load);
135170
}
171+
172+
bool HTMLScriptElement::isReadyToExecute() const
173+
{
174+
return scriptCompiled && !scriptExecutedOnce && compiledScript != nullptr;
175+
}
176+
177+
bool HTMLScriptElement::executeScriptFromQueue()
178+
{
179+
if (isReadyToExecute())
180+
{
181+
executeScript();
182+
return true;
183+
}
184+
else
185+
{
186+
return false;
187+
}
188+
}
136189
}

src/client/html/html_script_element.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ namespace dom
7474
void scheduleScriptExecution();
7575
void executeScript();
7676

77+
public:
78+
/**
79+
* Check if the script is ready to execute (compiled and not yet executed).
80+
* Used by the BrowsingContext execution queue.
81+
*/
82+
bool isReadyToExecute() const;
83+
84+
/**
85+
* Execute the script from the execution queue.
86+
* This bypasses the normal execution checks since the queue manages ordering.
87+
*
88+
* @returns Whether the script was executed.
89+
*/
90+
bool executeScriptFromQueue();
91+
7792
public:
7893
/**
7994
* A boolean value that controls how the script should be executed. For classic scripts, if the async property is set to true, the external
@@ -115,5 +130,9 @@ namespace dom
115130
bool scriptCompiled = false;
116131
bool scriptExecutedOnce = false;
117132
bool scriptExecutionScheduled = false;
133+
134+
// Whether the script uses the execution queue
135+
bool usesExecutionQueue = false;
136+
size_t scriptExecutionId = 0;
118137
};
119138
}

0 commit comments

Comments
 (0)