quoid / userscripts

An open-source userscript manager for Safari
https://quoid.github.io/userscripts/
GNU General Public License v3.0
3.22k stars 184 forks source link

After the await operation, other functions is cancelled due to delay. #636

Closed F9y4ng closed 6 months ago

F9y4ng commented 6 months ago

code

// ==UserScript==
// @name           NewScript-pegqasky
// @description    This is your new file, start writing code
// @match          *://*/*
// @grant          GM.getValue
// @run-at         document-start
// ==/UserScript==

(function() {
    class RegisterEvents {
        constructor() {
            this.fns = [];
            this.finalfns = [];
            this.done = false;
            this.__register();
            document.addEventListener("readystatechange", () => this.__checkReadyState());
        }
        __runFns(fns) {
            let run = 0;
            let err = 0;
            for (let fn of fns) {
                try {
                    fn();
                    run++;
                } catch(e) {
                    err++;
                }
            }
            console.log(run, err, document.readyState);
        }
        __register() {
            if (this.done) return;
            if (document.readyState === "loading") {
                document.addEventListener("DOMContentLoaded", () => this.__runFns(this.fns));
            } else {
                window.addEventListener("load", () =>this.__runFns(this.fns));
            }
            this.done = true;
        }
        __checkReadyState() {
            if (!this.done || document.readyState !== "complete") return;
            this.__runFns(this.finalfns);
        }
        addFn(fn) {
            if (typeof fn !== "function") return;
            this.fns.push(fn);
        }
        addFinalFn(fn) {
            if (typeof fn !== "function") return;
            this.finalfns.push(fn);
        }
    }

    const addEvent = new RegisterEvents();
     !(async function() {
        addEvent.addFinalFn(() =>console.log("00000"));
        await GM.getValue("x");
        await GM.getValue("x");
        await GM.getValue("x");

        addEvent.addFn(() =>console.log("11111"));
        await GM.getValue("x");
        await GM.getValue("x");
        await GM.getValue("x");

        addEvent.addFn(() =>console.log("22222"));
        await GM.getValue("x");
        await GM.getValue("x");
        await GM.getValue("x");

        addEvent.addFinalFn(() =>console.log("33333"));
    })();
})()

result

  1. visit www.baidu.com
  2. open console

Normal in other script managers

Normal

Error in userscripts

Error

ACTCD commented 6 months ago

It looks like the relevant events have occurred before the functions are registered.

I'm not sure what you expected and "functions is canceled" means here, but await does not block the page from loading.

If that's about script injection delay that we can't resolve due to Safari limitations, please refer to: #459 #184

F9y4ng commented 6 months ago

Tampermonkey and Stay2 work fine in Safari, only userscripts not.

I suggest you run the above code on different script managers locally, and then give a solution. Thanks.

ACTCD commented 6 months ago

I've added a few lines of debug logs to make the process clearer:

// ==UserScript==
// @name           NewScript-pegqasky
// @description    This is your new file, start writing code
// @match          *://*/*
// @grant          GM.getValue
// @run-at         document-start
// ==/UserScript==

(function() {
    class RegisterEvents {
        constructor() {
            this.fns = [];
            this.finalfns = [];
            this.done = false;
            this.__register();
            document.addEventListener("readystatechange", () => this.__checkReadyState());
        }
        __runFns(fns) {
            let run = 0;
            let err = 0;
            for (let fn of fns) {
                try {
                    fn();
                    run++;
                } catch(e) {
                    err++;
                }
            }
            console.log(run, err, document.readyState);
        }
        __register() {
            if (this.done) return;
            if (document.readyState === "loading") {
                document.addEventListener("DOMContentLoaded", () => this.__runFns(this.fns));
            } else {
                window.addEventListener("load", () =>this.__runFns(this.fns));
            }
            this.done = true;
        }
        __checkReadyState() {
            if (!this.done || document.readyState !== "complete") return;
            this.__runFns(this.finalfns);
        }
        addFn(fn) {
            if (typeof fn !== "function") return;
            this.fns.push(fn);
        }
        addFinalFn(fn) {
            if (typeof fn !== "function") return;
            this.finalfns.push(fn);
        }
    }

    document.addEventListener("readystatechange", () => console.debug(document.readyState));
    const addEvent = new RegisterEvents();
     !(async function() {
        console.debug("addFn0")
        addEvent.addFinalFn(() =>console.log("00000"));
        await GM.getValue("x");
        await GM.getValue("x");
        await GM.getValue("x");

        console.debug("addFn1")
        addEvent.addFn(() =>console.log("11111"));
        await GM.getValue("x");
        await GM.getValue("x");
        await GM.getValue("x");

        console.debug("addFn2")
        addEvent.addFn(() =>console.log("22222"));
        await GM.getValue("x");
        await GM.getValue("x");
        await GM.getValue("x");

        console.debug("addFn3")
        addEvent.addFinalFn(() =>console.log("33333"));
    })();
})()

On repeated refreshes I can get different results:

[Info] Injecting: NewScript-pegqasky (js/content) (userscripts.js, line 1)
[Debug] addFn0
[Debug] interactive
[Debug] addFn1
[Debug] addFn2
[Debug] addFn3
[Log] 11111
[Log] 22222
[Log] 2 – 0 – "interactive"
[Debug] complete
[Log] 00000
[Log] 33333
[Log] 2 – 0 – "complete"
[Info] Injecting: NewScript-pegqasky (js/content) (userscripts.js, line 1)
[Debug] addFn0
[Debug] complete
[Log] 00000
[Log] 1 – 0 – "complete"
[Log] 0 – 0 – "complete"
[Debug] addFn1
[Debug] addFn2
[Debug] addFn3
[Info] Injecting: NewScript-pegqasky (js/content) (userscripts.js, line 1)
[Debug] addFn0
[Debug] addFn1
[Debug] addFn2
[Debug] complete
[Log] 00000
[Log] 1 – 0 – "complete"
[Log] 11111
[Log] 22222
[Log] 2 – 0 – "complete"
[Debug] addFn3
[Info] Injecting: NewScript-pegqasky (js/content) (userscripts.js, line 1)
[Debug] addFn0
[Debug] addFn1
[Debug] addFn2
[Debug] addFn3

I don't see any issues with either outcome. These are normal result of asynchronous execution.

F9y4ng commented 6 months ago

The results of code running on different sites in safari and userscripts are also different.

In www.google.com, the correct result 1 will be returned, but in www.baidu.com, the incorrect result 2 will be returned, no matter how many times it is refreshed.

I mean, the delay caused by await operation did not make the function event successfully registered.

Userscripts follow the GM.* API of Greasemonkey, but running the above code on Greasemonkey of FF will still return the correct result 1.

So, I think the userscripts caused a delay in processing await operation, which made the subsequent events not registered successfully. This can be known by logging this.fns.length and this.finalfns.length.

ACTCD commented 6 months ago

I did not test on other websites, but only on the www.baidu.com you mentioned, and I got the above different results.

I don't find anything wrong with printing their length.

console.debug(addEvent.fns.length, addEvent.finalfns.length);
F9y4ng commented 6 months ago

From the four results you gave, except for the first one, which returned the correct result, everything else is problematic.

The problem is that it is too late to execute the .addFn() method. Some results are even executed after the 'complete' readystate, so when __runFns() is executed, there is no data in this.fns and this.finalfns at all.

The reason for the late execution is the await operation, and everything is normal without any await operations.

I don't know if I described it clearly.😅

ACTCD commented 6 months ago

Async awaits are certainly a factor in creating delays, and because of that there is some delay in user script injection itself. Because we have to use some asynchronous API to get the scripts data.

And because of these asynchronous characteristics, there is no issues with the above various results. It just doesn't match what you expected.

F9y4ng commented 6 months ago

Greasemonkey also use asynchronous API, but in most cases, it can complete the registration of this.finalfns before the 'complete' readystate, but userscripts can't, and it will fail every time.

If can't handle this problem in Safari, just give it up.Thanks for your reply!

ACTCD commented 6 months ago

We could certainly optimize our script preparation process to reduce latency a bit, as we mentioned in other issues.

But you should never expect the order of asynchronous execution, even if it appears in some order most of the time.