Browse Source

ref: improve methods for DataStore and DataStoreSerializer

Sven 9 months ago
parent
commit
ba9c9159f4
2 changed files with 60 additions and 47 deletions
  1. 44 23
      lib/DataStore.ts
  2. 16 24
      lib/DataStoreSerializer.ts

+ 44 - 23
lib/DataStore.ts

@@ -58,6 +58,9 @@ export type DataStoreOptions<TData> = {
  * Manages a hybrid synchronous & asynchronous persistent JSON database that is cached in memory and persistently saved across sessions using [GM storage.](https://wiki.greasespot.net/GM.setValue)  
  * Supports migrating data from older format versions to newer ones and populating the cache with default data if no persistent data is found.  
  *   
+ * All methods are at least `protected`, so you can easily extend this class and overwrite them to use a different storage method or to add additional functionality.  
+ * Remember that you can call `super.methodName()` in the subclass to access the original method.  
+ *   
  * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`  
  * ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
  * 
@@ -99,19 +102,19 @@ export class DataStore<TData extends object = object> {
    */
   public async loadData(): Promise<TData> {
     try {
-      const gmData = await GM.getValue(`_uucfg-${this.id}`, this.defaultData);
-      let gmFmtVer = Number(await GM.getValue(`_uucfgver-${this.id}`, NaN));
+      const gmData = await this.getValue(`_uucfg-${this.id}`, this.defaultData);
+      let gmFmtVer = Number(await this.getValue(`_uucfgver-${this.id}`, NaN));
 
       if(typeof gmData !== "string") {
         await this.saveDefaultData();
         return { ...this.defaultData };
       }
 
-      const isEncoded = await GM.getValue(`_uucfgenc-${this.id}`, false);
+      const isEncoded = await this.getValue(`_uucfgenc-${this.id}`, false);
 
       let saveData = false;
       if(isNaN(gmFmtVer)) {
-        await GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
+        await this.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
         saveData = true;
       }
 
@@ -136,9 +139,12 @@ export class DataStore<TData extends object = object> {
   /**
    * Returns a copy of the data from the in-memory cache.  
    * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
+   * @param deepCopy Whether to return a deep copy of the data (default: `false`) - only necessary if your data object is nested and may have a bigger performance impact if enabled
    */
-  public getData(): TData {
-    return this.deepCopy(this.cachedData);
+  public getData(deepCopy = false): TData {
+    return deepCopy
+      ? this.deepCopy(this.cachedData)
+      : { ...this.cachedData };
   }
 
   /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
@@ -147,9 +153,9 @@ export class DataStore<TData extends object = object> {
     const useEncoding = this.encodingEnabled();
     return new Promise<void>(async (resolve) => {
       await Promise.all([
-        GM.setValue(`_uucfg-${this.id}`, await this.serializeData(data, useEncoding)),
-        GM.setValue(`_uucfgver-${this.id}`, this.formatVersion),
-        GM.setValue(`_uucfgenc-${this.id}`, useEncoding),
+        this.setValue(`_uucfg-${this.id}`, await this.serializeData(data, useEncoding)),
+        this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
+        this.setValue(`_uucfgenc-${this.id}`, useEncoding),
       ]);
       resolve();
     });
@@ -161,9 +167,9 @@ export class DataStore<TData extends object = object> {
     const useEncoding = this.encodingEnabled();
     return new Promise<void>(async (resolve) => {
       await Promise.all([
-        GM.setValue(`_uucfg-${this.id}`, await this.serializeData(this.defaultData, useEncoding)),
-        GM.setValue(`_uucfgver-${this.id}`, this.formatVersion),
-        GM.setValue(`_uucfgenc-${this.id}`, useEncoding),
+        this.setValue(`_uucfg-${this.id}`, await this.serializeData(this.defaultData, useEncoding)),
+        this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
+        this.setValue(`_uucfgenc-${this.id}`, useEncoding),
       ]);
       resolve();
     });
@@ -178,9 +184,9 @@ export class DataStore<TData extends object = object> {
    */
   public async deleteData(): Promise<void> {
     await Promise.all([
-      GM.deleteValue(`_uucfg-${this.id}`),
-      GM.deleteValue(`_uucfgver-${this.id}`),
-      GM.deleteValue(`_uucfgenc-${this.id}`),
+      this.deleteValue(`_uucfg-${this.id}`),
+      this.deleteValue(`_uucfgver-${this.id}`),
+      this.deleteValue(`_uucfgenc-${this.id}`),
     ]);
   }
 
@@ -222,9 +228,9 @@ export class DataStore<TData extends object = object> {
     }
 
     await Promise.all([
-      GM.setValue(`_uucfg-${this.id}`, await this.serializeData(newData)),
-      GM.setValue(`_uucfgver-${this.id}`, lastFmtVer),
-      GM.setValue(`_uucfgenc-${this.id}`, this.encodingEnabled()),
+      this.setValue(`_uucfg-${this.id}`, await this.serializeData(newData)),
+      this.setValue(`_uucfgver-${this.id}`, lastFmtVer),
+      this.setValue(`_uucfgenc-${this.id}`, this.encodingEnabled()),
     ]);
 
     return this.cachedData = { ...newData as TData };
@@ -236,9 +242,9 @@ export class DataStore<TData extends object = object> {
   }
 
   /** Serializes the data using the optional this.encodeData() and returns it as a string */
-  private async serializeData(data: TData, useEncoding = true): Promise<string> {
+  protected async serializeData(data: TData, useEncoding = true): Promise<string> {
     const stringData = JSON.stringify(data);
-    if(!this.encodeData || !this.decodeData || !useEncoding)
+    if(!this.encodingEnabled() || !useEncoding)
       return stringData;
 
     const encRes = this.encodeData(stringData);
@@ -248,7 +254,7 @@ export class DataStore<TData extends object = object> {
   }
 
   /** Deserializes the data using the optional this.decodeData() and returns it as a JSON object */
-  private async deserializeData(data: string, useEncoding = true): Promise<TData> {
+  protected async deserializeData(data: string, useEncoding = true): Promise<TData> {
     let decRes = this.encodingEnabled() && useEncoding ? this.decodeData(data) : undefined;
     if(decRes instanceof Promise)
       decRes = await decRes;
@@ -256,8 +262,23 @@ export class DataStore<TData extends object = object> {
     return JSON.parse(decRes ?? data) as TData;
   }
 
-  /** Copies a JSON-compatible object and loses its internal references */
-  private deepCopy<T>(obj: T): T {
+  /** Copies a JSON-compatible object and loses all its internal references in the process */
+  protected deepCopy<T>(obj: T): T {
     return JSON.parse(JSON.stringify(obj));
   }
+
+  /** Gets a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
+  protected async getValue<TValue = GM.Value>(name: string, defaultValue: TValue): Promise<TValue> {
+    return GM.getValue<TValue>(name, defaultValue);
+  }
+
+  /** Sets a value in persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
+  protected async setValue(name: string, value: GM.Value): Promise<void> {
+    return GM.setValue(name, value);
+  }
+
+  /** Deletes a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
+  protected async deleteValue(name: string): Promise<void> {
+    return GM.deleteValue(name);
+  }
 }

+ 16 - 24
lib/DataStoreSerializer.ts

@@ -1,4 +1,4 @@
-import { getUnsafeWindow, type DataStore } from "./index.js";
+import { getUnsafeWindow, computeHash, type DataStore } from "./index.js";
 
 export type DataStoreSerializerOptions = {
   /** Whether to add a checksum to the exported data */
@@ -23,7 +23,11 @@ export type SerializedDataStore = {
 
 /**
  * Allows for easy serialization and deserialization of multiple DataStore instances.  
- * Needs to run in a secure context (HTTPS) due to the use of the Web Crypto API.
+ *   
+ * All methods are at least `protected`, so you can easily extend this class and overwrite them to use a different storage method or to add additional functionality.  
+ * Remember that you can call `super.methodName()` in the subclass to access the original method.  
+ *   
+ * ⚠️ Needs to run in a secure context (HTTPS) due to the use of the Web Crypto API.  
  */
 export class DataStoreSerializer {
   protected stores: DataStore[];
@@ -43,31 +47,23 @@ export class DataStoreSerializer {
 
   /** Calculates the checksum of a string */
   protected async calcChecksum(input: string): Promise<string> {
-    const encoder = new TextEncoder();
-    const data = encoder.encode(input);
-
-    const hashBuffer = await crypto.subtle.digest("SHA-256", data);
-
-    const hashArray = Array.from(new Uint8Array(hashBuffer));
-    const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
-
-    return hashHex;
+    return computeHash(input, "SHA-256");
   }
 
   /** Serializes a DataStore instance */
-  protected async serializeStore(store: DataStore): Promise<SerializedDataStore> {
-    const data = store.encodingEnabled()
-      ? await store.encodeData(JSON.stringify(store.getData()))
-      : JSON.stringify(store.getData());
+  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: store.id,
+      id: storeInst.id,
       data,
-      formatVersion: store.formatVersion,
-      encoded: store.encodingEnabled(),
+      formatVersion: storeInst.formatVersion,
+      encoded: storeInst.encodingEnabled(),
       checksum,
     };
   }
@@ -104,14 +100,10 @@ export class DataStoreSerializer {
         ? await storeInst.decodeData(storeData.data)
         : storeData.data;
 
-      if(storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion) {
-        console.log("[BetterYTM/#DEBUG] UU - running migrations");
+      if(storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
         await storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
-      }
-      else {
-        console.log("[BetterYTM/#DEBUG] UU - setting directly", JSON.parse(decodedData));
+      else
         await storeInst.setData(JSON.parse(decodedData));
-      }
     }
   }
 }