|
@@ -2,20 +2,35 @@
|
|
|
|
|
|
/** Function that takes the data in the old format and returns the data in the new format. Also supports an asynchronous migration. */
|
|
|
type MigrationFunc = <TOldData = any>(oldData: TOldData) => any | Promise<any>;
|
|
|
-/** Dictionary of format version numbers and the function that migrates from them to the next whole integer. */
|
|
|
+/** Dictionary of format version numbers and the function that migrates to them from the previous whole integer */
|
|
|
type MigrationsDict = Record<number, MigrationFunc>;
|
|
|
|
|
|
+/** Options for the ConfigManager instance */
|
|
|
export interface ConfigManagerOptions<TData> {
|
|
|
- /** A unique ID for this configuration */
|
|
|
+ /** A unique internal ID for this configuration - choose wisely as changing it is not supported yet. */
|
|
|
id: string;
|
|
|
- /** The default config data to use if no data is saved in persistent storage yet. Until the data is loaded from persistent storage, this will be the data returned by `getData()` */
|
|
|
+ /**
|
|
|
+ * The default config data object to use if no data is saved in persistent storage yet.
|
|
|
+ * Until the data is loaded from persistent storage with `loadData()`, this will be the data returned by `getData()`
|
|
|
+ *
|
|
|
+ * ⚠️ This has to be an object that can be serialized to JSON, so no functions or circular references are allowed, they will cause unexpected behavior.
|
|
|
+ */
|
|
|
defaultConfig: TData;
|
|
|
- /** An incremental version of the data format. If the format of the data is changed, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively. Never decrement this number, but you may skip numbers if you need to for some reason. */
|
|
|
+ /**
|
|
|
+ * An incremental, whole integer version number of the current format of config data.
|
|
|
+ * If the format of the data is changed, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively.
|
|
|
+ *
|
|
|
+ * ⚠️ Never decrement this number and optimally don't skip any numbers either!
|
|
|
+ */
|
|
|
formatVersion: number;
|
|
|
- /** A dictionary of functions that can be used to migrate data from older versions of the configuration to newer ones. The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value. The values should be functions that take the data in the old format and return the data in the new format. The functions will be run in order from the oldest to the newest version. If the current format version is not in the dictionary, no migrations will be run. */
|
|
|
+ /**
|
|
|
+ * A dictionary of functions that can be used to migrate data from older versions of the configuration to newer ones.
|
|
|
+ * The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value.
|
|
|
+ * The values should be functions that take the data in the old format and return the data in the new format.
|
|
|
+ * The functions will be run in order from the oldest to the newest version.
|
|
|
+ * If the current format version is not in the dictionary, no migrations will be run.
|
|
|
+ */
|
|
|
migrations?: MigrationsDict;
|
|
|
- /** If set to true, the already stored data in persistent storage is loaded asynchronously as soon as this instance is created. Note that this might cause race conditions as it is uncertain when the internal data cache gets populated. */
|
|
|
- autoLoad?: boolean;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -24,7 +39,7 @@ export interface ConfigManagerOptions<TData> {
|
|
|
*
|
|
|
* ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
|
|
|
*
|
|
|
- * @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 `options.formatVersion`
|
|
|
+ * @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`
|
|
|
*/
|
|
|
export class ConfigManager<TData = any> {
|
|
|
public readonly id: string;
|
|
@@ -35,7 +50,8 @@ export class ConfigManager<TData = any> {
|
|
|
|
|
|
/**
|
|
|
* Creates an instance of ConfigManager.
|
|
|
- * Make sure to call `loadData()` after creating an instance if you didn't set `autoLoad` to true.
|
|
|
+ *
|
|
|
+ * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
|
|
|
* @param options The options for this ConfigManager instance
|
|
|
*/
|
|
|
constructor(options: ConfigManagerOptions<TData>) {
|
|
@@ -44,12 +60,13 @@ export class ConfigManager<TData = any> {
|
|
|
this.defaultConfig = options.defaultConfig;
|
|
|
this.cachedConfig = options.defaultConfig;
|
|
|
this.migrations = options.migrations;
|
|
|
-
|
|
|
- if(options.autoLoad === true)
|
|
|
- this.loadData();
|
|
|
}
|
|
|
|
|
|
- /** Loads the data saved in persistent storage into the in-memory cache and also returns it. Automatically populates persistent storage with default data if it doesn't contain data yet. */
|
|
|
+ /**
|
|
|
+ * Loads the data saved in persistent storage into the in-memory cache and also returns it.
|
|
|
+ * Automatically populates persistent storage with default data if it doesn't contain any data yet.
|
|
|
+ * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
|
|
|
+ */
|
|
|
public async loadData(): Promise<TData> {
|
|
|
try {
|
|
|
const gmData = await GM.getValue(this.id, this.defaultConfig);
|
|
@@ -112,7 +129,7 @@ export class ConfigManager<TData = any> {
|
|
|
|
|
|
/** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
|
|
|
protected async runMigrations(oldData: any, oldFmtVer: number): Promise<TData> {
|
|
|
- console.info("#DEBUG#-- RUNNING MIGRATIONS", oldFmtVer, "->", this.formatVersion, "- oldData:", oldData);
|
|
|
+ console.info("#DEBUG - RUNNING MIGRATIONS", oldFmtVer, "->", this.formatVersion, "- oldData:", oldData);
|
|
|
|
|
|
if(!this.migrations)
|
|
|
return oldData as TData;
|
|
@@ -125,9 +142,14 @@ export class ConfigManager<TData = any> {
|
|
|
for(const [fmtVer, migrationFunc] of sortedMigrations) {
|
|
|
const ver = Number(fmtVer);
|
|
|
if(oldFmtVer < this.formatVersion && oldFmtVer < ver) {
|
|
|
- const migRes = migrationFunc(newData);
|
|
|
- newData = migRes instanceof Promise ? await migRes : migRes;
|
|
|
- oldFmtVer = ver;
|
|
|
+ try {
|
|
|
+ const migRes = migrationFunc(newData);
|
|
|
+ newData = migRes instanceof Promise ? await migRes : migRes;
|
|
|
+ oldFmtVer = ver;
|
|
|
+ }
|
|
|
+ catch(err) {
|
|
|
+ console.error(`Error while running migration function for format version ${fmtVer}:`, err);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -135,7 +157,8 @@ export class ConfigManager<TData = any> {
|
|
|
return newData as TData;
|
|
|
}
|
|
|
|
|
|
- protected deepCopy<T>(obj: T): T {
|
|
|
+ /** Copies a JSON-compatible object and loses its internal references */
|
|
|
+ private deepCopy<T>(obj: T): T {
|
|
|
return JSON.parse(JSON.stringify(obj));
|
|
|
}
|
|
|
}
|