Переглянути джерело

ref: better cfg mgr documentation

Sven 1 рік тому
батько
коміт
f232f9674b
3 змінених файлів з 50 додано та 28 видалено
  1. 2 3
      README.md
  2. 41 18
      lib/config.ts
  3. 7 7
      lib/misc.ts

+ 2 - 3
README.md

@@ -525,11 +525,10 @@ Also supports automatic migration of outdated data formats via provided migratio
 The options object has the following properties:
 | Property | Description |
 | :-- | :-- |
-| `id` | A unique ID for this configuration |
+| `id` | A unique internal identification string for this configuration. If two ConfigManagers share the same ID, they will overwrite each other's data. Choose wisely because if it is changed, the previously saved data will not be able to be loaded anymore. |
 | `defaultConfig` | 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()`. For TypeScript, the type of the data passed here is what will be used for all other methods of the instance. |
 | `formatVersion` | An incremental version of the data format. If the format of the data is changed in any way, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively. Never decrement this number or skip numbers. |
 | `migrations?` | (Optional) 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. |
-| `autoLoad?` | (Optional) 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. |
   
 ⚠️ The configuration is stored as a JSON string, so only JSON-compatible data can be used.  
 ⚠️ The directives `@grant GM.getValue` and `@grant GM.setValue` are required for this to work.  
@@ -575,7 +574,7 @@ const migrations = {
 };
 
 const configMgr = new ConfigManager({
-  /** A unique ID for this configuration */
+  /** A unique ID for this configuration - choose wisely as changing it is not supported yet! */
   id: "my-userscript",
   /** Default / fallback configuration data */
   defaultConfig,

+ 41 - 18
lib/config.ts

@@ -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));
   }
 }

+ 7 - 7
lib/misc.ts

@@ -1,9 +1,3 @@
-/** Options for the `fetchAdvanced()` function */
-export type FetchAdvancedOpts = RequestInit & Partial<{
-  /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
-  timeout: number;
-}>;
-
 /**
  * Automatically appends an `s` to the passed `word`, if `num` is not equal to 1
  * @param word A word in singular form, to auto-convert to plural
@@ -23,7 +17,7 @@ export function pauseFor(time: number) {
 }
 
 /**
- * Calls the passed `func` after the specified `timeout` in ms.  
+ * Calls the passed `func` after the specified `timeout` in ms (defaults to 300).  
  * Any subsequent calls to this function will reset the timer and discard previous calls.
  */
 export function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(func: TFunc, timeout = 300) { // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -34,6 +28,12 @@ export function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(
   };
 }
 
+/** Options for the `fetchAdvanced()` function */
+export type FetchAdvancedOpts = RequestInit & Partial<{
+  /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
+  timeout: number;
+}>;
+
 /** Calls the fetch API with special options like a timeout */
 export async function fetchAdvanced(url: string, options: FetchAdvancedOpts = {}) {
   const { timeout = 10000 } = options;