Skip to content
Prev Previous commit
Next Next commit
Remove cyclic object
  • Loading branch information
LinXunFeng committed Mar 12, 2024
commit 182afac743fe51d6dfbb97bc13bb5ff0037a138b
Original file line number Diff line number Diff line change
Expand Up @@ -1457,9 +1457,13 @@ Future<void> main() async {
<title>WebResourceError test</title>
<script type="text/javascript">
function onLoad() {
const circularReference = { otherData: 123 };
circularReference.myself = circularReference;
console.log(circularReference);
const obj = {
a: "Flutter",
b: {},
};
obj.self = obj;
obj.b.self = obj;
console.log(obj);
}
</script>
</head>
Expand Down Expand Up @@ -1490,7 +1494,7 @@ Future<void> main() async {
));

await expectLater(debugMessageReceived.future,
completion('log:{"otherData":123,"myself":"[Circular]"}'));
completion('log:{"a":"Flutter","b":{}}'));
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,31 +633,24 @@ class WebKitWebViewController extends PlatformWebViewController {
// Using the replacer parameter of JSON.stringify() to solve the error
// TypeError: JSON.stringify cannot serialize cyclic structures.
// See https://github.com/flutter/flutter/issues/144535.
//
// The getCircularReplacer() taken from
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value.
//
// Considering this is just looking at the logs printed via console.log,
// the cyclic object is not important, so remove it.
const WKUserScript overrideScript = WKUserScript(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment briefly explaining the approach at a high level so readers don't need to reverse-engineer what is being done here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

'''
function getCircularReplacer() {
const ancestors = [];
return function (key, value) {
if (typeof value !== "object" || value === null) {
return value;
}
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
ancestors.pop();
}
if (ancestors.includes(value)) {
return "[Circular]";
}
ancestors.push(value);
return value;
function removeCyclicObject() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a name that cannot plausibly cause collisions on actual web pages, since it is injected into the page's context. E.g., maybe a Flutter prefix and a dynamically generated UUID? @ditman Any suggestions on best practice for avoiding naming collisions in JS?

Copy link
Member

@ditman ditman Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stuartmorgan At the expense of some performance this function could be defined inside of the log function that calls it (line 659), and that way it wouldn't be directly exposed to the host page... But if we're not worried about a function log colliding with the functions of a webpage, I wouldn't worry too much about removeCyclicObject either :)

(In the engine we normally prefix with _ the name of the function so there's a fewer change of a global collision, but there's no guarantee that it won't collide. For a UUID you'd have to strip the - characters for it to be a valid function name, but it could definitely work)

((PS: Generating a UUID in JS: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID, has just become "available" to Flutter web according to our Supported platforms))

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the expense of some performance this function could be defined inside of the log function that calls it (line 659), and that way it wouldn't be directly exposed to the host page

That sounds worth doing. If people are doing performance-affecting levels of print logging (and enabling this feature in release builds, which hopefully they aren't), they have problems already.

But if we're not worried about a function log colliding with the functions of a webpage, I wouldn't worry too much about removeCyclicObject either :)

😬 I am very concerned about that, I just missed it somehow. We should absolutely rename that.

Maybe UUID is overkill; we could also just do something like _flutter_log_override that's pretty unlikely to collide.

((PS: Generating a UUID in JS: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID, has just become "available" to Flutter web according to our Supported platforms))

We wouldn't need to do it in JS; we'd generate it in Dart, and then string interpolate that into the string we inject.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't need to do it in JS; we'd generate it in Dart, and then string interpolate that into the string we inject.

Ah you're right, it's "free" then. _flutter_log_override sounds like a good name. You can even have a small _flutter_webview_plugin_overrides "namespace" object that contains all the methods that you're injecting, so it's easy to hide/move them around.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const objects = new WeakSet();
return function (k, v) {
if (typeof v !== "object" || v === null) { return v; }
if (objects.has(v)) { return; }
objects.add(v);
return v;
};
}

function log(type, args) {
var message = Object.values(args)
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, getCircularReplacer()) : v.toString())
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, removeCyclicObject()) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1378,26 +1378,19 @@ void main() {
expect(overrideConsoleScript.injectionTime,
WKUserScriptInjectionTime.atDocumentStart);
expect(overrideConsoleScript.source, '''
function getCircularReplacer() {
const ancestors = [];
return function (key, value) {
if (typeof value !== "object" || value === null) {
return value;
}
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
ancestors.pop();
}
if (ancestors.includes(value)) {
return "[Circular]";
}
ancestors.push(value);
return value;
function removeCyclicObject() {
const objects = new WeakSet();
return function (k, v) {
if (typeof v !== "object" || v === null) { return v; }
if (objects.has(v)) { return; }
objects.add(v);
return v;
};
}

function log(type, args) {
var message = Object.values(args)
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, getCircularReplacer()) : v.toString())
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v, removeCyclicObject()) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ");

Expand Down