A lightweight, dependency-free mutex for Userscripts that ensures **only one tab / context** runs a critical section at a time. It coordinates through `GM.setValue` + `GM_addValueChangeListener`, so it works across multiple tabs, iframes, and even separate scripts that share the same @name/@namespace storage.
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/554436/1692608/GM_lock.js
A lightweight, dependency-free mutex for Userscripts that coordinates via GM.setValue + GM_addValueChangeListener, so only one tab/context runs a critical section at a time. Works across multiple tabs and iframes that share the same userscript storage.
Install / @require This is a library, not a standalone script. Include it from Greasy Fork with:
// @require https://update.greasyfork.org/scripts/554436/1692608/GM_lock.js(Use the latest build number from the Code tab.)
// JavaScript
var GM_lock: (tag: string, func: () => (void | Promise<void>)) => Promise<void>;
returns: A Promise<void> that resolves when func completes.
func are caught and logged to the console; they do not reject the promise.Note: The current implementation does not forward a return value from
func(it always resolves withvoid). If you need a result, assign it to outer scope or storage insidefunc.
// Only one instance across all tabs will enter this block at a time
await GM_lock('sync-cache', async () => {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
// Do something with data; persist as needed
await GM.setValue('cache:data', data);
});
Use short, stable tags per feature: e.g. 'queue:upload', 'cache:refresh'.
Requires a userscript manager that supports:
GM.setValueGM_addValueChangeListenerGM_removeValueChangeListenerOptional/conditional (only if cleanup is enabled; see Options):
GM.deleteValues (or the legacy GM_deleteValues alias)Tested with ScriptCat, Tampermonkey and Violentmonkey; should also work wherever the above APIs are available.
Each contender constructs a lock id id = <bigTimestamp>_<randomBase36> and uses per-tag keys with the prefix:
ack key: GM_lock::<tag>::acknxt key: GM_lock::<tag>::nxtA contender announces itself by writing to ack and then relays through nxt.
Peers collect contenders with the same ack time and, after a small collision interval (default ~500 ms), deterministically elect a winner by sorting the collected ids and choosing the smallest. The winner runs func.
After running, the winner updates ack again to release the lock and allow the next election round if needed.
This event-driven design minimizes polling and coordinates cleanly across tabs/frames.
COLLISION_INTERVAL = 500 — wait before electing a winner to reduce race windows.CLEANUP_VALUES = false — when true, remove helper keys after use (requires GM.deleteValues).SHOW_LOG = false — when true, emits timing markers via GM.setValue (debug).func; prefer await-based work. (Other contenders coordinate on events and short timers.) func if necessary.Public domain — The Unlicense. See the header in the source.