|
@@ -37,7 +37,8 @@ export interface ConfigManagerOptions<TData> {
|
|
|
* Manages a user configuration that is cached in memory and persistently saved across sessions.
|
|
|
* Supports migrating data from older versions of the configuration to newer ones and populating the cache with default data if no persistent data is found.
|
|
|
*
|
|
|
- * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
|
|
|
+ * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
|
|
|
+ * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
|
|
|
*
|
|
|
* @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `config.defaultConfig`) - this should also be the type of the data format associated with the current `options.formatVersion`
|
|
|
*/
|
|
@@ -49,11 +50,15 @@ export class ConfigManager<TData = any> {
|
|
|
private migrations?: MigrationsDict;
|
|
|
|
|
|
/**
|
|
|
- * Creates an instance of ConfigManager.
|
|
|
+ * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
|
|
|
+ * Supports migrating data from older versions of the configuration to newer ones and populating the cache with default data if no persistent data is found.
|
|
|
*
|
|
|
+ * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
|
|
|
* ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
|
|
|
+ *
|
|
|
+ * @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `config.defaultConfig`) - this should also be the type of the data format associated with the current `options.formatVersion`
|
|
|
* @param options The options for this ConfigManager instance
|
|
|
- */
|
|
|
+ */
|
|
|
constructor(options: ConfigManagerOptions<TData>) {
|
|
|
this.id = options.id;
|
|
|
this.formatVersion = options.formatVersion;
|
|
@@ -69,14 +74,16 @@ export class ConfigManager<TData = any> {
|
|
|
*/
|
|
|
public async loadData(): Promise<TData> {
|
|
|
try {
|
|
|
- const gmData = await GM.getValue(this.id, this.defaultConfig);
|
|
|
- let gmFmtVer = Number(await GM.getValue(`_uufmtver-${this.id}`));
|
|
|
+ const gmData = await GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
|
|
|
+ let gmFmtVer = Number(await GM.getValue(`_uucfgver-${this.id}`));
|
|
|
|
|
|
- if(typeof gmData !== "string")
|
|
|
- return await this.saveDefaultData();
|
|
|
+ if(typeof gmData !== "string") {
|
|
|
+ await this.saveDefaultData();
|
|
|
+ return this.defaultConfig;
|
|
|
+ }
|
|
|
|
|
|
if(isNaN(gmFmtVer))
|
|
|
- await GM.setValue(`_uufmtver-${this.id}`, gmFmtVer = this.formatVersion);
|
|
|
+ await GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
|
|
|
|
|
|
let parsed = JSON.parse(gmData);
|
|
|
|
|
@@ -86,7 +93,8 @@ export class ConfigManager<TData = any> {
|
|
|
return this.cachedConfig = typeof parsed === "object" ? parsed : undefined;
|
|
|
}
|
|
|
catch(err) {
|
|
|
- return await this.saveDefaultData();
|
|
|
+ await this.saveDefaultData();
|
|
|
+ return this.defaultConfig;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -98,33 +106,39 @@ export class ConfigManager<TData = any> {
|
|
|
/** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
|
|
|
public setData(data: TData) {
|
|
|
this.cachedConfig = data;
|
|
|
- return new Promise<TData>(async (resolve) => {
|
|
|
- await GM.setValue(this.id, JSON.stringify(data));
|
|
|
- await GM.setValue(`_uufmtver-${this.id}`, this.formatVersion);
|
|
|
- resolve(data);
|
|
|
+ return new Promise<void>(async (resolve) => {
|
|
|
+ await Promise.allSettled([
|
|
|
+ GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
|
|
|
+ GM.setValue(`_uucfgver-${this.id}`, this.formatVersion),
|
|
|
+ ]);
|
|
|
+ resolve();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
|
|
|
public async saveDefaultData() {
|
|
|
this.cachedConfig = this.defaultConfig;
|
|
|
- return new Promise<TData>(async (resolve) => {
|
|
|
- await GM.setValue(this.id, JSON.stringify(this.defaultConfig));
|
|
|
- await GM.setValue(`_uufmtver-${this.id}`, this.formatVersion);
|
|
|
- resolve(this.defaultConfig);
|
|
|
+ return new Promise<void>(async (resolve) => {
|
|
|
+ await Promise.allSettled([
|
|
|
+ GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
|
|
|
+ GM.setValue(`_uucfgver-${this.id}`, this.formatVersion),
|
|
|
+ ]);
|
|
|
+ resolve();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Call this method to clear all persistently stored data associated with this ConfigManager instance.
|
|
|
- * The in-memory cache will be left untouched, so you may still access the data with `getData()`
|
|
|
+ * The in-memory cache will be left untouched, so you may still access the data with `getData()`.
|
|
|
* Calling `loadData()` or `setData()` after this method was called will recreate persistent storage with the cached or default data.
|
|
|
*
|
|
|
* ⚠️ This requires the additional directive `@grant GM.deleteValue`
|
|
|
*/
|
|
|
public async deleteConfig() {
|
|
|
- await GM.deleteValue(this.id);
|
|
|
- await GM.deleteValue(`_uufmtver-${this.id}`);
|
|
|
+ await Promise.allSettled([
|
|
|
+ GM.deleteValue(`_uucfg-${this.id}`),
|
|
|
+ GM.deleteValue(`_uucfgver-${this.id}`),
|
|
|
+ ]);
|
|
|
}
|
|
|
|
|
|
/** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
|
|
@@ -132,20 +146,19 @@ export class ConfigManager<TData = any> {
|
|
|
if(!this.migrations)
|
|
|
return oldData as TData;
|
|
|
|
|
|
- console.info("#DEBUG - RUNNING MIGRATIONS", oldFmtVer, "->", this.formatVersion, "- oldData:", oldData);
|
|
|
-
|
|
|
- // TODO: verify
|
|
|
let newData = oldData;
|
|
|
const sortedMigrations = Object.entries(this.migrations)
|
|
|
.sort(([a], [b]) => Number(a) - Number(b));
|
|
|
|
|
|
+ let lastFmtVer = oldFmtVer;
|
|
|
+
|
|
|
for(const [fmtVer, migrationFunc] of sortedMigrations) {
|
|
|
const ver = Number(fmtVer);
|
|
|
if(oldFmtVer < this.formatVersion && oldFmtVer < ver) {
|
|
|
try {
|
|
|
const migRes = migrationFunc(newData);
|
|
|
newData = migRes instanceof Promise ? await migRes : migRes;
|
|
|
- oldFmtVer = ver;
|
|
|
+ lastFmtVer = oldFmtVer = ver;
|
|
|
}
|
|
|
catch(err) {
|
|
|
console.error(`Error while running migration function for format version ${fmtVer}:`, err);
|
|
@@ -153,7 +166,11 @@ export class ConfigManager<TData = any> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- await GM.setValue(`_uufmtver-${this.id}`, this.formatVersion);
|
|
|
+ await Promise.allSettled([
|
|
|
+ GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
|
|
|
+ GM.setValue(`_uucfgver-${this.id}`, lastFmtVer),
|
|
|
+ ]);
|
|
|
+
|
|
|
return newData as TData;
|
|
|
}
|
|
|
|