Greasy Fork

Greasy Fork is available in English.

Monkey Storage

Useful library for dealing with the storage.

当前为 2020-06-22 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/405831/819177/Monkey%20Storage.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name Monkey Storage
// @namespace https://rafaelgssa.gitlab.io/monkey-scripts
// @version 1.0.2
// @author rafaelgssa
// @description Useful library for dealing with the storage.
// @match *://*/*
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require https://unpkg.com/uuid@latest/dist/umd/uuidv4.min.js
// @require http://greasyfork.icu/scripts/405813-monkey-utils/code/Monkey%20Utils.js
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// ==/UserScript==

/* global MonkeyUtils, uuidv4 */

/**
 * @typedef {Object} MonkeyStorageLock
 * @property {string} uuid
 * @property {string} key
 * @property {number} [timeoutId]
 * @typedef {Object} MonkeyStorageLocked
 * @property {string} uuid
 * @property {number} timestamp
 * @typedef {() => Promise<void>} MonkeyStorageRelease
 * @typedef {{
 *   [K: string]: unknown,
 *   settings: Record<string, unknown>
 * }} MonkeyStorageValues
 */

// eslint-disable-next-line
const MonkeyStorage = (() => {
	let _id = '';

	/** @type {MonkeyStorageValues} */
	const _defaultValues = {
		settings: {},
	};

	/** @type {MonkeyStorageValues} */
	const _cache = {
		settings: {},
	};

	/**
	 * Initializes the storage.
	 * @param {string} id The ID to use for the local storage.
	 * @param {Partial<MonkeyStorageValues>} [defaultValues] Any default values to set.
	 * @returns {Promise<void>}
	 */
	const init = (id, defaultValues) => {
		_id = id;
		if (MonkeyUtils.isSet(defaultValues)) {
			for (const [key, value] of Object.entries(defaultValues)) {
				setDefaultValue(key, value);
			}
		}
		return _updateCache('settings');
	};

	/**
	 * Sets a default value.
	 * @param {string} key The key of the default value to set.
	 * @param {unknown} value The default value to set.
	 */
	const setDefaultValue = (key, value) => {
		_defaultValues[key] = value;
	};

	/**
	 * Sets a value in the storage.
	 * @param {string} key The key of the value to set.
	 * @param {unknown} value The value to set.
	 * @returns {Promise<void>}
	 */
	const setValue = async (key, value) => {
		const stringifiedValue = JSON.stringify(value);
		await GM.setValue(key, stringifiedValue);
		_cache[key] = value;
	};

	/**
	 * Gets a value from the cache.
	 * @param {string} key The key of the value to get.
	 * @param {boolean} [updateCache] Whether to update the cache with the storage or not.
	 * @returns {Promise<unknown>} The value.
	 */
	const getValue = async (key, updateCache = false) => {
		if (!MonkeyUtils.isSet(_cache[key]) || updateCache) {
			await _updateCache(key);
		}
		return _cache[key];
	};

	/**
	 * Updates a value in the cache with the storage.
	 * @param {string} key The key of the value to update.
	 * @returns {Promise<void>}
	 */
	const _updateCache = async (key) => {
		let value = await GM.getValue(key);
		if (typeof value === 'string') {
			try {
				value = JSON.parse(value);
			} catch (err) {
				// Value is already parsed, just ignore.
			}
		}
		_cache[key] = MonkeyUtils.isSet(value) ? value : _defaultValues[key];
	};

	/**
	 * Deletes a value from the storage.
	 * @param {string} key The key of the value to delete.
	 * @returns {Promise<void>}
	 */
	const deleteValue = async (key) => {
		await GM.deleteValue(key);
		delete _cache[key];
	};

	/**
	 * Sets a value in the local storage.
	 * @param {string} key The key of the value to set.
	 * @param {unknown} value The value to set.
	 */
	const setLocalValue = (key, value) => {
		const stringifiedValue = JSON.stringify(value);
		window.localStorage.setItem(`${_id}_${key}`, stringifiedValue);
		_cache[key] = value;
	};

	/**
	 * Gets a value from the cache.
	 * @param {string} key The key of the value to get.
	 * @param {boolean} [updateCache] Whether to update the cache with the local storage or not.
	 * @returns {unknown} The value.
	 */
	const getLocalValue = (key, updateCache = false) => {
		if (!MonkeyUtils.isSet(_cache[key]) || updateCache) {
			_updateLocalCache(key);
		}
		return _cache[key];
	};

	/**
	 * Updates a value in the cache with the local storage.
	 * @param {string} key The key of the value to update.
	 */
	const _updateLocalCache = (key) => {
		let value = window.localStorage.getItem(`${_id}_${key}`);
		if (typeof value === 'string') {
			try {
				value = JSON.parse(value);
			} catch (err) {
				// Value is already parsed, just ignore.
			}
		}
		_cache[key] = MonkeyUtils.isSet(value) ? value : _defaultValues[key];
	};

	/**
	 * Deletes a value from the local storage.
	 * @param {string} key The key of the value to delete.
	 */
	const deleteLocalValue = (key) => {
		window.localStorage.removeItem(`${_id}_${key}`);
		delete _cache[key];
	};

	/**
	 * Sets a default setting.
	 * @param {string} key The key of the default setting to set.
	 * @param {unknown} setting The default setting to set.
	 */
	const setDefaultSetting = (key, setting) => {
		_defaultValues.settings[key] = setting;
	};

	/**
	 * Sets a setting in the cache.
	 * @param {string} key The key of the setting to set.
	 * @param {unknown} setting The setting to set.
	 */
	const setSetting = (key, setting) => {
		_cache.settings[key] = setting;
	};

	/**
	 * Gets a setting from the cache.
	 * @param {string} key The key of the setting to get.
	 * @param {boolean} [updateCache] Whether to update the settings cache with the storage or not.
	 * @returns {Promise<unknown>} The setting.
	 */
	const getSetting = async (key, updateCache = false) => {
		if (isSettingsEmpty() || !MonkeyUtils.isSet(_cache.settings[key]) || updateCache) {
			await _updateCache('settings');
		}
		return _cache.settings[key];
	};

	/**
	 * Deletes a setting from the cache.
	 * @param {string} key The key of the setting to delete.
	 */
	const deleteSetting = (key) => {
		delete _cache.settings[key];
	};

	/**
	 * Saves the settings from the cache.
	 * @returns {Promise<void>}
	 */
	const saveSettings = () => {
		return setValue('settings', _cache.settings);
	};

	/**
	 * Checks if the settings cache is empty.
	 * @returns {boolean} Whether the settings cache is empty or not.
	 */
	const isSettingsEmpty = () => {
		return Object.keys(_cache.settings).length === 0;
	};

	/**
	 * Creates a lock in the storage to prevent other tabs from executing concurrently.
	 * @param {string} key The key of the lock.
	 * @returns {Promise<MonkeyStorageRelease | undefined>} A function to release the lock, if successful.
	 */
	const createLock = async (key) => {
		const uuid = uuidv4();
		/** @type {MonkeyStorageLock} */
		const lock = { uuid, key };
		/** @type {MonkeyStorageLocked} */
		let locked;
		_logLockProgress('Trying to create', lock);
		let value = await GM.getValue(key);
		locked = JSON.parse(MonkeyUtils.isSet(value) ? value.toString() : '{}');
		if (locked.uuid && locked.uuid !== uuid && Date.now() - locked.timestamp > 5000) {
			_logLockProgress('Failed to create', lock);
			return;
		}
		_logLockProgress('Preparing to create', lock);
		await GM.setValue(key, `{ "uuid": "${uuid}", "timestamp": ${Date.now()} }`);
		await MonkeyUtils.sleep(1);
		value = await GM.getValue(key);
		locked = JSON.parse(MonkeyUtils.isSet(value) ? value.toString() : '{}');
		if (locked.uuid !== uuid) {
			_logLockProgress('Failed to create', lock);
			return;
		}
		_logLockProgress('Created', lock);
		_reinforceLock(lock, 1);
		return () => _releaseLock(lock);
	};

	/**
	 * Keeps reinforcing a lock.
	 * @param {MonkeyStorageLock} lock The lock to reinforce.
	 * @param {number} frequency How frequently to reinforce the lock.
	 */
	const _reinforceLock = async (lock, frequency) => {
		const { uuid, key } = lock;
		_logLockProgress('Reinforcing', lock);
		await GM.setValue(key, `{ "uuid": "${uuid}", "timestamp": ${Date.now()} }`);
		lock.timeoutId = window.setTimeout(_reinforceLock, frequency * 1000, lock, frequency);
	};

	/**
	 * Releases a lock.
	 * @param {MonkeyStorageLock} lock The lock to release.
	 * @returns {Promise<void>}
	 */
	const _releaseLock = async (lock) => {
		const { uuid, key, timeoutId } = lock;
		window.clearTimeout(timeoutId);
		let value = await GM.getValue(key);
		/** @type {MonkeyStorageLocked} */
		const locked = JSON.parse(MonkeyUtils.isSet(value) ? value.toString() : '{}');
		if (locked.uuid === uuid) {
			await deleteValue(key);
		}
		_logLockProgress('Released', lock);
	};

	/**
	 * @param {string} progress
	 * @param {MonkeyStorageLock} lock
	 */
	// eslint-disable-next-line
	const _logLockProgress = (progress, { uuid, key }) => {
		//console.log(`[${Date.now()}] ${progress} lock ${uuid}, ${key}`)
	};

	return {
		init,
		setDefaultValue,
		setValue,
		getValue,
		deleteValue,
		setLocalValue,
		getLocalValue,
		deleteLocalValue,
		setDefaultSetting,
		setSetting,
		getSetting,
		deleteSetting,
		saveSettings,
		isSettingsEmpty,
		createLock,
	};
})();