소스 검색

feat: custom error instances

Sv443 1 개월 전
부모
커밋
bf553357ee
12개의 변경된 파일159개의 추가작업 그리고 39개의 파일을 삭제
  1. 5 0
      .changeset/nervous-pets-scream.md
  2. 5 0
      .changeset/tender-actors-lie.md
  3. 5 0
      README-summary.md
  4. 5 0
      README.md
  5. 43 4
      docs.md
  6. 2 3
      lib/DataStore.ts
  7. 49 27
      lib/DataStoreSerializer.ts
  8. 1 1
      lib/Debouncer.ts
  9. 3 3
      lib/colors.ts
  10. 3 1
      lib/dom.ts
  11. 37 0
      lib/errors.ts
  12. 1 0
      lib/index.ts

+ 5 - 0
.changeset/nervous-pets-scream.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": minor
+---
+
+Changed a bunch of generic thrown `Error`s with the new custom error class instances

+ 5 - 0
.changeset/tender-actors-lie.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": minor
+---
+
+Added custom error classes `ChecksumMismatchError`, `DataMigrationError` and `PlatformError`, extending from the base class `UUError`

+ 5 - 0
README-summary.md

@@ -106,6 +106,11 @@ View the documentation of previous major releases:
     - [`ValueGen`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#valuegen) - a "generator" value that allows for super flexible value typing and declaration
     - [`StringGen`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#stringgen) - a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
     - [`ListWithLength`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#listwithlength) - represents an array or object with a numeric `length`, `count` or `size` property
+- **Custom Error classes:**
+    - [`UUError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#uuerror) - base class for all custom UserUtils errors - has a custom `date` prop set to the time of creation
+    - [`ChecksumMismatchError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#checksummismatcherror) - thrown when a string of data doesn't match its checksum
+    - [`DataMigrationError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datamigrationerror) - thrown when a data migration fails
+    - [`PlatformError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#platformerror) - thrown when a function is called in an unsupported environment
 
 <br><br>
 

+ 5 - 0
README.md

@@ -109,6 +109,11 @@ View the documentation of previous major releases:
     - [`ValueGen`](./docs.md#valuegen) - a "generator" value that allows for super flexible value typing and declaration
     - [`StringGen`](./docs.md#stringgen) - a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
     - [`ListWithLength`](./docs.md#listwithlength) - represents an array or object with a numeric `length`, `count` or `size` property
+  - [**Custom Error classes**](./docs.md#error-classes)
+    - [`UUError`](./docs.md#uuerror) - base class for all custom UserUtils errors - has a custom `date` prop set to the time of creation
+    - [`ChecksumMismatchError`](./docs.md#checksummismatcherror) - thrown when a string of data doesn't match its checksum
+    - [`DataMigrationError`](./docs.md#datamigrationerror) - thrown when a data migration fails
+    - [`PlatformError`](./docs.md#platformerror) - thrown when a function is called in an unsupported environment
 
 <br><br>
 

+ 43 - 4
docs.md

@@ -98,6 +98,11 @@ For submitting bug reports or feature requests, please use the [GitHub issue tra
     - [`ValueGen`](#valuegen) - a "generator" value that allows for super flexible value typing and declaration
     - [`StringGen`](#stringgen) - a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
     - [`ListWithLength`](#listwithlength) - represents an array or object with a numeric `length`, `count` or `size` property
+  - [**Custom Error classes**](#error-classes)
+    - [`UUError`](#uuerror) - base class for all custom UserUtils errors - has a custom `date` prop set to the time of creation
+    - [`ChecksumMismatchError`](#checksummismatcherror) - thrown when a string of data doesn't match its checksum
+    - [`DataMigrationError`](#datamigrationerror) - thrown when a data migration fails
+    - [`PlatformError`](#platformerror) - thrown when a function is called in an unsupported environment
 
 <br><br>
 
@@ -638,7 +643,7 @@ If no predicate is specified, all events will be discarded.
 Calling this function will set the `Error.stackTraceLimit` to 100 (if it's not already higher) to ensure the stack trace is preserved.  
   
 ⚠️ This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are *attached* after this function is called.  
-⚠️ Due to this function modifying the `addEventListener` prototype, it might break execution of the page's main script if the userscript is running in an isolated context (like it does in FireMonkey). In that case, calling this function will throw an error.  
+⚠️ Due to this function modifying the `addEventListener` prototype, it might break execution of the page's main script if the userscript is running in an isolated context (like it does in FireMonkey). In that case, calling this function will throw a [`PlatformError`](#platformerror).  
   
 <details><summary><b>Example - click to view</b></summary>
 
@@ -1329,7 +1334,7 @@ This is why you should either immediately repopulate the cache and persistent st
 #### `DataStore.runMigrations()`
 Signature: `runMigrations(oldData: any, oldFmtVer: number, resetOnError?: boolean): Promise<TData>`  
 Runs all necessary migration functions to migrate the given `oldData` to the latest format.  
-If `resetOnError` is set to `false`, the migration will be aborted if an error is thrown and no data will be committed. If it is set to `true` (default) and an error is encountered, it will be suppressed and the `defaultData` will be saved to persistent storage and returned.
+If `resetOnError` is set to `false`, the migration will be aborted and a [`DataMigrationError`](#datamigrationerror) is thrown and no data will be committed. If it is set to `true` (default) and an error is encountered, it will be suppressed and the `defaultData` will be saved to persistent storage and returned.
 
 <br>
 
@@ -1500,9 +1505,9 @@ Serializes all DataStore instances passed in the constructor and returns the ser
 #### `DataStoreSerializer.deserialize()`
 Signature: `deserialize(data: string): Promise<void>`  
 Deserializes the given string that was created with `serialize()` and imports the contained data each DataStore instance.  
-In the process of importing the data, the migrations will be run, if the `formatVersion` property is lower than the one set on the DataStore instance.
+In the process of importing the data, the migrations will be run, if the `formatVersion` property is lower than the one set on the DataStore instance.  
   
-If `ensureIntegrity` is set to `true` and the checksum doesn't match, an error will be thrown.  
+If `ensureIntegrity` is set to `true` and the checksum doesn't match, a [`ChecksumMismatchError`](#checksummismatcherror) will be thrown.  
 If `ensureIntegrity` is set to `false`, the checksum check will be skipped entirely.  
 If the `checksum` property is missing on the imported data, the checksum check will also be skipped.  
 If `encoded` is set to `true`, the data will be decoded using the `decodeData` function set on the DataStore instance.  
@@ -3541,6 +3546,38 @@ getSize([...iter]); // 1
 ```
 </details>
 
+<br><br>
+
+## Error classes:
+UserUtils has some custom error classes that make it easier to handle specific types of errors.  
+All of them extend the built-in `Error` class and have a `date` property that contains the date and time when the error was created.  
+These classes are intended to be used by the library, but if you find them useful, you can import them and throw them in your own code as well.  
+  
+<br>
+
+### UUError
+Base class for all UserUtils errors.  
+Extends from the built-in `Error` class.  
+Has the custom property `date` that holds the date and time when the error was created.
+
+<br>
+
+### ChecksumMismatchError
+Thrown when a checksum verification fails.  
+Extends from the `UUError` class.
+
+<br>
+
+### DataMigrationError
+Thrown when a data migration fails.  
+Extends from the `UUError` class.
+
+<br>
+
+### PlatformError
+Thrown when a platform-specific error occurs, like when a browser API call fails, or the browser doesn't support a feature at all.  
+Extends from the `UUError` class.
+
 <br><br><br><br>
 
 <!-- #region Footer -->
@@ -3550,3 +3587,5 @@ Made with ❤️ by [Sv443](https://github.com/Sv443)
 If you like this library, please consider [supporting development](https://github.com/sponsors/Sv443)
 
 </div>
+
+<br><br><br><br>

+ 2 - 3
lib/DataStore.ts

@@ -3,6 +3,7 @@
  * This module contains the DataStore class, which is a general purpose, sync and async persistent JSON database - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datastore)
  */
 
+import { MigrationError } from "./errors.js";
 import type { Prettify } from "./types.js";
 
 //#region types
@@ -271,9 +272,7 @@ export class DataStore<TData extends object = object> {
         }
         catch(err) {
           if(!resetOnError)
-            throw new Error(`Error while running migration function for format version '${fmtVer}'`);
-
-          console.error(`Error while running migration function for format version '${fmtVer}' - resetting to the default value.`, err);
+            throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err });
 
           await this.saveDefaultData();
           return this.getData();

+ 49 - 27
lib/DataStoreSerializer.ts

@@ -3,7 +3,10 @@
  * This module contains the DataStoreSerializer class, which allows you to import and export serialized DataStore data - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datastoreserializer)
  */
 
-import { getUnsafeWindow, computeHash, type DataStore } from "./index.js";
+import { computeHash } from "./crypto.js";
+import { getUnsafeWindow } from "./dom.js";
+import { ChecksumMismatchError } from "./errors.js";
+import type { DataStore } from "./DataStore.js";
 
 export type DataStoreSerializerOptions = {
   /** Whether to add a checksum to the exported data */
@@ -63,40 +66,54 @@ export class DataStoreSerializer {
     return computeHash(input, "SHA-256");
   }
 
-  /** Serializes a DataStore instance */
-  protected async serializeStore(storeInst: DataStore): Promise<SerializedDataStore> {
-    const data = storeInst.encodingEnabled()
-      ? await storeInst.encodeData(JSON.stringify(storeInst.getData()))
-      : JSON.stringify(storeInst.getData());
-    const checksum = this.options.addChecksum
-      ? await this.calcChecksum(data)
-      : undefined;
-
-    return {
-      id: storeInst.id,
-      data,
-      formatVersion: storeInst.formatVersion,
-      encoded: storeInst.encodingEnabled(),
-      checksum,
-    };
-  }
-
-  /** Serializes the data stores into a string */
-  public async serialize(): Promise<string> {
+  /**
+   * Serializes the data stores into a string.  
+   * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
+   * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
+   */
+  public async serialize(useEncoding: boolean, stringified: true): Promise<string>;
+  /**
+   * Serializes the data stores into a string.  
+   * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
+   * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
+   */
+  public async serialize(useEncoding: boolean, stringified: false): Promise<SerializedDataStore[]>;
+  /**
+   * Serializes the data stores into a string.  
+   * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
+   * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
+   */
+  public async serialize(useEncoding = true, stringified = true): Promise<string | SerializedDataStore[]> {
     const serData: SerializedDataStore[] = [];
 
-    for(const store of this.stores)
-      serData.push(await this.serializeStore(store));
+    for(const storeInst of this.stores) {
+      const data = useEncoding && storeInst.encodingEnabled()
+        ? await storeInst.encodeData(JSON.stringify(storeInst.getData()))
+        : JSON.stringify(storeInst.getData());
+
+      serData.push({
+        id: storeInst.id,
+        data,
+        formatVersion: storeInst.formatVersion,
+        encoded: useEncoding && storeInst.encodingEnabled(),
+        checksum: this.options.addChecksum
+          ? await this.calcChecksum(data)
+          : undefined,
+      });
+    }
 
-    return JSON.stringify(serData);
+    return stringified ? JSON.stringify(serData) : serData;
   }
 
   /**
    * Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.  
    * Also triggers the migration process if the data format has changed.
    */
-  public async deserialize(serializedData: string): Promise<void> {
-    const deserStores: SerializedDataStore[] = JSON.parse(serializedData);
+  public async deserialize(serializedData: string | SerializedDataStore[]): Promise<void> {
+    const deserStores: SerializedDataStore[] = typeof serializedData === "string" ? JSON.parse(serializedData) : serializedData;
+
+    if(!Array.isArray(deserStores) || !deserStores.every(DataStoreSerializer.isSerializedDataStore))
+      throw new TypeError("Invalid serialized data format! Expected an array of SerializedDataStore objects.");
 
     for(const storeData of deserStores) {
       const storeInst = this.stores.find(s => s.id === storeData.id);
@@ -106,7 +123,7 @@ export class DataStoreSerializer {
       if(this.options.ensureIntegrity && typeof storeData.checksum === "string") {
         const checksum = await this.calcChecksum(storeData.data);
         if(checksum !== storeData.checksum)
-          throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!\nExpected: ${storeData.checksum}\nHas: ${checksum}`);
+          throw new ChecksumMismatchError(`Checksum mismatch for DataStore with ID "${storeData.id}"!\nExpected: ${storeData.checksum}\nHas: ${checksum}`);
       }
 
       const decodedData = storeData.encoded && storeInst.encodingEnabled()
@@ -146,4 +163,9 @@ export class DataStoreSerializer {
   public async deleteStoresData(): Promise<PromiseSettledResult<void>[]> {
     return Promise.allSettled(this.stores.map(store => store.deleteData()));
   }
+
+  /** Checks if a given value is a SerializedDataStore object */
+  public static isSerializedDataStore(obj: unknown): obj is SerializedDataStore {
+    return typeof obj === "object" && obj !== null && "id" in obj && "data" in obj && "formatVersion" in obj && "encoded" in obj;
+  }
 }

+ 1 - 1
lib/Debouncer.ts

@@ -159,7 +159,7 @@ export class Debouncer<TFunc extends AnyFunc> extends NanoEmitter<DebouncerEvent
 
       break;
     default:
-      throw new Error(`Invalid debouncer type: ${this.type}`);
+      throw new TypeError(`Invalid debouncer type: ${this.type}`);
     }
   }
 }

+ 3 - 3
lib/colors.ts

@@ -70,11 +70,11 @@ export function darkenColor(color: string, percent: number, upperCase = false):
   else if(color.startsWith("rgb")) {
     const rgbValues = color.match(/\d+(\.\d+)?/g)?.map(Number);
     if(!rgbValues)
-      throw new Error("Invalid RGB/RGBA color format");
+      throw new TypeError("Invalid RGB/RGBA color format");
     [r, g, b, a] = rgbValues as [number, number, number, number?];
   }
   else
-    throw new Error("Unsupported color format");
+    throw new TypeError("Unsupported color format");
 
   [r, g, b] = darkenRgb(r, g, b, percent);
 
@@ -85,5 +85,5 @@ export function darkenColor(color: string, percent: number, upperCase = false):
   else if(color.startsWith("rgb"))
     return `rgb(${r}, ${g}, ${b})`;
   else
-    throw new Error("Unsupported color format");
+    throw new TypeError("Unsupported color format");
 }

+ 3 - 1
lib/dom.ts

@@ -3,6 +3,8 @@
  * This module contains various functions for working with the DOM - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#dom)
  */
 
+import { PlatformError } from "./errors.js";
+
 /** Whether the DOM has finished loading */
 let domReady = false;
 document.addEventListener("DOMContentLoaded", () => domReady = true);
@@ -116,7 +118,7 @@ export function interceptEvent<
 ): void {
   // @ts-ignore
   if(GM?.info?.scriptHandler && GM.info.scriptHandler === "FireMonkey" && (eventObject === window || eventObject === getUnsafeWindow()))
-    throw new Error("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
+    throw new PlatformError("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
 
   // default is 25 on FF so this should hopefully be more than enough
   // @ts-ignore

+ 37 - 0
lib/errors.ts

@@ -0,0 +1,37 @@
+/**
+ * @module lib/errors
+ * Contains custom error classes
+ */
+
+/** Base class for all UserUtils errors - adds a `date` prop set to the error throw time */
+export class UUError extends Error {
+  public readonly date: Date;
+  constructor(message: string, options?: ErrorOptions) {
+    super(message, options);
+    this.date = new Date();
+  }
+}
+
+/** Error while validating checksum */
+export class ChecksumMismatchError extends UUError {
+  constructor(message: string, options?: ErrorOptions) {
+    super(message, options);
+    this.name = "ChecksumMismatchError";
+  }
+}
+
+/** Error while migrating data */
+export class MigrationError extends UUError {
+  constructor(message: string, options?: ErrorOptions) {
+    super(message, options);
+    this.name = "MigrationError";
+  }
+}
+
+/** Error due to the platform, like using a feature that's not supported by the browser */
+export class PlatformError extends UUError {
+  constructor(message: string, options?: ErrorOptions) {
+    super(message, options);
+    this.name = "PlatformError";
+  }
+}

+ 1 - 0
lib/index.ts

@@ -11,6 +11,7 @@ export * from "./DataStoreSerializer.js";
 export * from "./Debouncer.js";
 export * from "./Dialog.js";
 export * from "./dom.js";
+export * from "./errors.js";
 export * from "./math.js";
 export * from "./misc.js";
 export * from "./NanoEmitter.js";