2

This is a brainstorming question, looking for ideas.

Is there any method to convert a sync function in an external script to behave like async.

Consider the situation of userscript & userscript manager.

The userscript uses a sync GM_getValue function and userscript developers are reluctant to update the code to async GM.getValue API.

The script manger that is processing the script, can not support sync method.

Is there any way to handle the code in an async way?

Example:

function run() {

  const a = GM_getValue('one');
}
  • Is there a possibility to halt the function by script manager until async response is available?
  • Is there a way to parse the script and convert the relevant functions to async/await? (The Regex is error prone)
  • Is there a way to override the function and replace it with an async version?
    e.g.
async function run() {

  const a = await GM_getValue('one');
}
  • Any other ideas?

Update example subsequent to comments

// ==UserScript==
// @name            My Script
// @match           http://www.example.org/*
// @grant           GM_getValue
// ==/UserScript==

function run() {
  const b = GM_getValue("one"); 
  if (b && b > a) {
    // do somthig
  }
  else {
    // do something else
  }
  return b;
}

const a = 5;
const c = run();
const d = GM_getValue("two"); 
const e = parseInt(d);

if (d > a) {
  // do somthig
}
else {
  // do something else
}

function sum(a, b) {
  return a + b;
}

const f = sum(2, 5);
erosman
  • 7,094
  • 7
  • 27
  • 46

1 Answers1

1

Is there a possibility to halt the function by script manager until async response is available?

No. The thing that comes closest would be sync XHR, but that's deprecated and should be avoided.

There is no easy one-size-fits-all solution. A regular expression definitely won't be sufficient. You will either have to:

  • Go through the script, identify all uses of the API that needs to be async, and refactor the control flow to account for it. While this might be a bit tedious, it's pretty rote and should be easy for those with experience working with the language.

  • Use a different synchronous method instead. This is what I'd prefer. Instead of using GM_getValue, consider saving the userscript settings in Local Storage instead. This is a usually a good choice unless the data is sensitive and the site is untrustworthy. For example, instead of

    const a = GM_getValue('one');
    

    you could do

    const userscriptSettings = JSON.parse(localStorage.userscriptSettings || '{}');
    const a = userscriptSettings.a;
    

Sometimes local storage won't suffice, such as when saving huge amounts of data, or saving the data where the site has the theoretical ability to change it is something to worry about, in which case you'll have to take the other option involving async control flow refactoring.

For the example in the question, I'd change it to:

const runScript = (userscriptSettings) => {
  function run() {
    const b = userscriptSettings.one;
    // ...
  }
  const a = 5;
  const c = run();
  const d = userscriptSettings.two;
  // ...
};
Promise.all([
  GM.getValue('one'),
  GM.getValue('two'),
])
  .then(([one, two]) => runScript({ one, two }));
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • The localStorage is not possible since a userscript can run on multiple domains and saving will be done to a single domain and data will not be available in other domains. – erosman Dec 02 '20 at 18:39
  • You can decide on a single domain to save the settings to and then [communicate between the domains with a hidden iframe](https://stackoverflow.com/questions/41111556/tampermonkey-message-between-scripts-on-different-subdomains/61052335#61052335) controlled by the userscript. It's finicky, but [it's an option](https://github.com/CertainPerformance/Stack-Exchange-Userscripts/blob/master/Speak-New-Questions/src/questionListPage/setupCrossDomainCommunication.ts) if you don't want to go the async refactoring route. – CertainPerformance Dec 02 '20 at 18:41
  • communicate between the domains with a hidden iframe is an async method (postMessage + addEventListener). – erosman Dec 02 '20 at 18:53
  • Oh right. If you need cross-domain storage, it looks like you'll have to work on async refactoring then. But in most situations it should be pretty easy: just change the entry point to get the userscript settings, *then* run the rest of the code, without any changes to the rest of it. – CertainPerformance Dec 02 '20 at 18:58
  • The whole issue is that the original function is sync ;) – erosman Dec 02 '20 at 19:05
  • Then some refactoring will be necessary - but like I said, it should be pretty easy in most cases. Eg, wrap the whole script in a function, then do `GM_getValue(...).then(runScript)` or use `Promise.all` if more are needed. It'll be more complicated if you need to set *and immediately afterwards synchronously get* values values saved in storage that aren't saved in standalone variables. Either refactor to handle async, or save the values in persistent variables in addition to saving to storage. – CertainPerformance Dec 02 '20 at 19:10
  • There could be multiple `GM_getValue()`, and could be within other functions. `GM_getValue().then()` will not help the rest of the code that runs before this promised is resolved.` – erosman Dec 02 '20 at 19:43
  • If there are multiple, then like I said, use `Promise.all` to wait for all to resolve. If they're within other functions, I'd recommend refactoring to retrieve the data only *once*, in the entry point. If that doesn't suit your purposes, then you'll have to go through it more manually and refactor for the async control flow that's needed. There's no easy magic bullet for all situations. – CertainPerformance Dec 02 '20 at 19:53
  • Please check the example added to the post. – erosman Dec 02 '20 at 20:29
  • Wrapping the whole script in a function looks like a simple tweak for that one – CertainPerformance Dec 02 '20 at 20:51
  • That wont work. Wrapping the whole and adding async will not apply to `GM_getValue` inside `run()`. That `run()` would also need an `async` plus `await` before `GM_getValue`. Then there is the `GM_getValue` which is not inside a function, that would need an `await`. – erosman Dec 02 '20 at 21:55
  • Sure it will. The asynchronous logic is taken care of at the entry point with the `Promise.all`, then the results get passed to the main script with the `runScript` function. The `runScript` can remain completely synchronous - like in the answer, use `userscriptSettings.one` instead of `GM_getValue('one')`. – CertainPerformance Dec 02 '20 at 22:06
  • The `userscriptSettings.one` will be *the value* you want, not a Promise. – CertainPerformance Dec 02 '20 at 22:06
  • Can you show me a complete code to see what you mean? – erosman Dec 03 '20 at 07:47
  • The code in my answer does exactly that. `Promise.all([ GM.getValue('one'), GM.getValue('two'), ]) .then(([one, two]) => runScript({ one, two }));` and then `runScript` will be called with an object with `one` and `two` properties containing the values you want. – CertainPerformance Dec 03 '20 at 13:56
  • The point of the post seems to be missed which is the script code is not controlled by the script-manager. The solution must be a solution that works for any unknown script that uses sync `GM_getValue('xyz')` in any situation, nested or not. – erosman Dec 03 '20 at 14:32
  • No easy general solution exists, as I've said. The script will have to be changed manually, unless someone trains a machine learning model to do it pretty accurately. – CertainPerformance Dec 03 '20 at 14:56