Skip to content

Asynchronous read and write migration

sizzlemctwizzle edited this page Jun 18, 2023 · 74 revisions

Intro

This page covers the general and full impact on users of GM_config due to the switch to asynchronous reading and writing of stored values. This is a direct result of supporting the Greasemonkey 4+ APIs. Why did your script seem to be working just fine in Greasemonkey? GM_config had been falling back to localStorage in Greasemonkey, so it wouldn't seem like there was an impact.

Firstly, remember to use the proper @grant lines in the your script metadata:

// ==UserScript==
// @name               Script Name
// @namespace          Script Namespace
// @require            https://openuserjs.org/src/libs/sizzle/GM_config.js
//
// Legacy GM3 support
// @grant              GM_getValue
// @grant              GM_setValue
// Current GM4 support
// @grant              GM.getValue
// @grant              GM.setValue
// ==/UserScript==

General Impact

Note: Code examples use initialization via the constructor, but GM_config.init is still fully supported.

Initialization is now done asynchronously, which means this will no longer work:

let gmc = new GM_config(
{
  'id': 'ThisConfig',
  'fields': {
    'val': {
      'label': 'Value',
      'type': 'text'
    }
  }
});
// initialization is not complete
// value is not yet available
let val = gmc.get('val');

Now get needs to be moved into the init event callback function:

let gmc = new GM_config(
{
  'id': 'ThisConfig',
  'fields': {
    'val': {
      'label': 'Value',
      'type': 'text'
    }
  },
  'events': {
    'init': () => {
      // initialization complete
      // value is now available
      let val = gmc.get('val');
    }
  }
});

Another way to write this is:

let gmc = new GM_config(
{
  'id': 'ThisConfig',
  'fields': {
    'val': {
      'label': 'Value',
      'type': 'text'
    }
  },
  'events': {
    'init': onInit
  }
});

function onInit() {
  // initialization complete
  // value is now available
  let val = gmc.get('val');
}

Finally, using a Promise:

let gmc = new GM_config(
{
  'id': 'ThisConfig',
  'fields': {
    'val_1': {
      'label': 'Value 1',
      'type': 'text'
    },
    'val_2': {
      'label': 'Value 2',
      'type': 'text'
    }
  }
});

// Promise resolves when initialization completes
let onInit = config => new Promise(resolve => {
  let isInit = () => setTimeout(() => 
    config.isInit ? resolve() : isInit(), 0);
  isInit();
});

// Generate a Promise
let init = onInit(gmc);

// Break up get() calls
init.then(() => {
  // initialization complete
  // value is now available
  let val = gmc.get('val_1');
});

init.then(() => {
  // initialization complete
  // value is now available
  let val = gmc.get('val_2');
});
Extra Example: break up get calls without a Promise
// Register multiple callbacks as a single callback function
function Callbacks() {
  let hasRun = false;
  let runables = [];
  let next = () => runables.shift();
  let run = function () {
    hasRun = true;
    for (let fn = next(); !!fn; fn = next())
      setTimeout(fn);
  };
  run.add = fn => {
    runables.push(fn);
    if (hasRun) run();
  };
  return run;
}

// Get a single callback function
let onInit = new Callbacks();

let gmc = new GM_config(
{
  'id': 'ThisConfig',
  'fields': {
    'val_1': {
      'label': 'Value 1',
      'type': 'text'
    },
    'val_2': {
      'label': 'Value 2',
      'type': 'text'
    }
  },
  'events': {
    'init': onInit
  }
});

// Break-up get() calls
onInit.add(() => {
  // initialization complete
  // value is now available
  let val = gmc.get('val_1');
});

onInit.add(() => {
  // initialization complete
  // value is now available
  let val = gmc.get('val_2');
});

Full Impact

This section covers impact on advance usage. Most users can just skip this.

Potential impact on advance usage:

  • getValue, setValue, log, read, write, save, and init are all asynchronous now (open was already asynchronous).
    • setValue, log, write, and save can be used like before in most cases.
    • getValue, read, and init are impacted the most.
    • I consider getValue, setValue, log, read, and write to be internal so I don't count them as a breaking change.
  • save and init are the official API functions that are affected by a breaking change, even though save won't be impacted in most usages.
  • Although not asynchronous, get fails when used before init has finished.

The async execution paths are: init -> read -> getValue and save -> write -> setValue. Both getValue and setValue use a Promise, whereas read and write use callback functions (last argument).

Note: init refers to initialization that can be reach via: GM_config.init, GM_config, new GM_config, GM_configStruct, new GM_configStruct , configInstance.init

Clone this wiki locally