Selaa lähdekoodia

feat: add DataStoreSerializer

Sven 9 kuukautta sitten
vanhempi
commit
1e2015c
3 muutettua tiedostoa jossa 123 lisäystä ja 0 poistoa
  1. 5 0
      .changeset/fair-snakes-tan.md
  2. 117 0
      lib/DataStoreSerializer.ts
  3. 1 0
      lib/index.ts

+ 5 - 0
.changeset/fair-snakes-tan.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": major
+---
+
+Added `DataStoreSerializer` class for centralized and much easier and safer de-/serialization of any number of DataStore instances

+ 117 - 0
lib/DataStoreSerializer.ts

@@ -0,0 +1,117 @@
+import { getUnsafeWindow, type DataStore } from ".";
+
+export type DataStoreSerializerOptions = {
+  /** Whether to add a checksum to the exported data */
+  addChecksum?: boolean;
+  /** Whether to ensure the integrity of the data when importing it (unless the checksum property doesn't exist) */
+  ensureIntegrity?: boolean;
+};
+
+/** Serialized data of a DataStore instance */
+export type SerializedDataStore = {
+  /** The ID of the DataStore instance */
+  id: string;
+  /** The serialized data */
+  data: string;
+  /** The format version of the data */
+  formatVersion: number;
+  /** Whether the data is encoded */
+  encoded: boolean;
+  /** The checksum of the data - key is not present for data without a checksum */
+  checksum?: string;
+};
+
+/**
+ * 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.
+ */
+export class DataStoreSerializer {
+  protected stores: DataStore[];
+  protected options: Required<DataStoreSerializerOptions>;
+
+  constructor(stores: DataStore[], options: DataStoreSerializerOptions = {}) {
+    if(!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
+      throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
+
+    this.stores = stores;
+    this.options = {
+      addChecksum: true,
+      ensureIntegrity: true,
+      ...options,
+    };
+  }
+
+  /** 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;
+  }
+
+  /** 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());
+    const checksum = this.options.addChecksum
+      ? await this.calcChecksum(data)
+      : undefined;
+
+    return {
+      id: store.id,
+      data,
+      formatVersion: store.formatVersion,
+      encoded: store.encodingEnabled(),
+      checksum,
+    };
+  }
+
+  /** Serializes the data stores into a string */
+  public async serialize(): Promise<string> {
+    const serData: SerializedDataStore[] = [];
+
+    for(const store of this.stores)
+      serData.push(await this.serializeStore(store));
+
+    return JSON.stringify(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);
+
+    for(const storeData of deserStores) {
+      const storeInst = this.stores.find(s => s.id === storeData.id);
+      if(!storeInst)
+        throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
+
+      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}`);
+      }
+
+      const decodedData = storeData.encoded && storeInst.encodingEnabled()
+        ? 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");
+        await storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
+      }
+      else {
+        console.log("[BetterYTM/#DEBUG] UU - setting directly", JSON.parse(decodedData));
+        await storeInst.setData(JSON.parse(decodedData));
+      }
+    }
+  }
+}

+ 1 - 0
lib/index.ts

@@ -1,5 +1,6 @@
 export * from "./array";
 export * from "./DataStore";
+export * from "./DataStoreSerializer";
 export * from "./dom";
 export * from "./math";
 export * from "./misc";