Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: script async can be delayed, error event trigger without exception #1358

Merged
merged 4 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bridge/bindings/qjs/dom/elements/script_element.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ interface Element {}

interface ScriptElement extends Element {
src: string;
async: boolean;
defer: boolean;
type: string;
charset: string;
text: string;
}
3 changes: 2 additions & 1 deletion bridge/bindings/qjs/dom/event_target.cc
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ bool EventTargetInstance::internalDispatchEvent(EventInstance* eventInstance) {

// Dispatch event listener white by 'on' prefix property.
if (m_eventHandlerMap.contains(eventType)) {
auto* window = static_cast<EventTargetInstance*>(JS_GetOpaque(context()->global(), 1));
// Let special error event handling be true if event is an ErrorEvent.
bool specialErrorEventHanding = eventTypeStr == "error";
bool specialErrorEventHanding = eventTypeStr == "error" && eventInstance->currentTarget() == window;

if (specialErrorEventHanding) {
auto _dispatchErrorEvent = [&eventInstance, this, eventTypeStr](JSValue handler) {
Expand Down
2 changes: 2 additions & 0 deletions integration_tests/assets/defineA.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
window.A = 'A';
window.bundleALoadTime = Date.now();
2 changes: 2 additions & 0 deletions integration_tests/assets/defineB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
window.B = 'B';
window.bundleBLoadTime = Date.now();
32 changes: 31 additions & 1 deletion integration_tests/specs/dom/elements/script.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

describe('script element', () => {
it('should work with src', async (done) => {
const p = <p>Should see hello below:</p>;
Expand All @@ -11,4 +10,35 @@ describe('script element', () => {
done();
};
});

it('load failed with error event', (done) => {
const script = document.createElement('script');
document.body.appendChild(script);
script.onerror = () => {
done();
};
script.src = 'http://127.0.0.1/path/to/a/file';
});

it('async script execute in delayed order', async (done) => {
const scriptA = document.createElement('script');
scriptA.async = true;
scriptA.src = 'assets:///assets/defineA.js';

const scriptB = document.createElement('script');
scriptB.src = 'assets:///assets/defineB.js';

document.body.appendChild(scriptA);
document.body.appendChild(scriptB);

scriptA.onload = () => {
// expect bundle B has already loaded.
expect(window.A).toEqual('A');
expect(window.B).toEqual('B');

// Bundle B load earlier than A.
expect(window.bundleALoadTime - window.bundleBLoadTime > 0).toEqual(true);
done();
};
});
});
2 changes: 2 additions & 0 deletions kraken/example/assets/defineA.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
window.A = 'A';
window.bundleALoadTime = Date.now();
2 changes: 2 additions & 0 deletions kraken/example/assets/defineB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
window.B = 'B';
window.bundleBLoadTime = Date.now();
103 changes: 65 additions & 38 deletions kraken/lib/src/dom/elements/script.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ const String _MIME_APPLICATION_JAVASCRIPT = 'application/javascript';
const String _MIME_X_APPLICATION_JAVASCRIPT = 'application/x-javascript';
const String _JAVASCRIPT_MODULE = 'module';

typedef ScriptExecution = void Function(bool async);

class ScriptRunner {
ScriptRunner(Document document, int contextId) : _document = document, _contextId = contextId;
final Document _document;
final int _contextId;

final List<KrakenBundle> _scriptsToExecute = [];
final List<KrakenBundle> _asyncScriptsToExecute = [];
final List<ScriptExecution> _syncScriptTasks = [];
final List<ScriptExecution> _asyncScriptTasks = [];

static void _evaluateScriptBundle(int contextId, KrakenBundle bundle, { bool async = false }) async {
// Evaluate bundle.
Expand All @@ -41,47 +43,53 @@ class ScriptRunner {
}
}

void _executeScripts(List<KrakenBundle> scripts, { bool async = false }) async {
while (_scriptsToExecute.isNotEmpty) {
KrakenBundle bundle = _scriptsToExecute.first;
void _execute(List<ScriptExecution> tasks, { bool async = false }) {
List<ScriptExecution> executingTasks = [...tasks];
tasks.clear();

for (ScriptExecution task in executingTasks) {
task(async);
}
}

void _queueScriptForExecution(ScriptElement element) async {
// Increment load event delay count before eval.
_document.incrementLoadEventDelayCount();

String url = element.src.toString();

// Obtain bundle.
KrakenBundle bundle = KrakenBundle.fromUrl(url);

// The bundle execution task.
void task(bool async) {
// If bundle is not resolved, should wait for it resolve to prevent the next script running.
if (!bundle.isResolved) break;
if (!bundle.isResolved) return;

try {
_evaluateScriptBundle(_contextId, bundle, async: async);
} catch (err, stack) {
print('$err\n$stack');
debugPrint('$err\n$stack');
bundle.dispose();
_document.decrementLoadEventDelayCount();
rethrow;
}

_scriptsToExecute.remove(bundle);

bundle.dispose();

// Decrement load event delay count after eval.
_document.decrementLoadEventDelayCount();
}
}

void _executeAsyncScripts(List<KrakenBundle> asyncScripts) {
_executeScripts(asyncScripts, async: true);
}

void queueScriptForExecution(ScriptElement element) async {
// Increment load event delay count before eval.
_document.incrementLoadEventDelayCount();

String url = element.src.toString();

// Obtain bundle.
KrakenBundle bundle = KrakenBundle.fromUrl(url);

if (element.async || element.defer) {
_asyncScriptsToExecute.add(bundle);
// @TODO: Differ async and defer.
final bool shouldAsync = element.async || element.defer;
if (shouldAsync) {
_asyncScriptTasks.add(task);
} else {
_scriptsToExecute.add(bundle);
_syncScriptTasks.add(task);
}

// Script loading phrase.
try {
// Increment count when request.
_document.incrementRequestCount();
Expand All @@ -91,20 +99,41 @@ class ScriptRunner {

// Decrement count when response.
_document.decrementRequestCount();

_executeScripts(_scriptsToExecute);

// Successful load.
Timer.run(() {
element.dispatchEvent(Event(EVENT_LOAD));
compute(_executeAsyncScripts, _asyncScriptsToExecute);
});
} catch (e, st) {
// An error occurred.
// A load error occurred.
debugPrint('Failed to load script: $url, reason: $e\n$st');
Timer.run(() {
element.dispatchEvent(Event(EVENT_ERROR));
});
_document.decrementLoadEventDelayCount();
return;
}

// Script executing phrase.
if (shouldAsync) {
// @TODO: Use requestIdleCallback
SchedulerBinding.instance!.scheduleFrameCallback((_) {
try {
_execute(_asyncScriptTasks, async: true);
} catch (error, stack) {
debugPrint('$error\n$stack');
} finally {
Timer.run(() {
element.dispatchEvent(Event(EVENT_LOAD));
});
}
});
} else {
try {
_execute(_syncScriptTasks, async: false);
} catch (error, stack) {
debugPrint('$error\n$stack');
} finally {
// Always emit load event.
Timer.run(() {
element.dispatchEvent(Event(EVENT_LOAD));
});
}
}
}
}
Expand Down Expand Up @@ -167,7 +196,6 @@ class ScriptElement extends Element {
// Set src will not reflect to attribute src.
}

// @TODO: implement async.
bool get async => getAttribute('async') != null;
set async(bool value) {
if (value) {
Expand All @@ -177,7 +205,6 @@ class ScriptElement extends Element {
}
}

// @TODO: implement defer.
bool get defer => getAttribute('defer') != null;
set defer(bool value) {
if (value) {
Expand Down Expand Up @@ -223,7 +250,7 @@ class ScriptElement extends Element {
|| _type == _JAVASCRIPT_MODULE
)) {
// Add bundle to scripts queue.
ownerDocument.scriptRunner.queueScriptForExecution(this);
ownerDocument.scriptRunner._queueScriptForExecution(this);

SchedulerBinding.instance!.scheduleFrame();
}
Expand Down