1
0

DataStoreSerializer.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { getUnsafeWindow, type DataStore } from ".";
  2. export type DataStoreSerializerOptions = {
  3. /** Whether to add a checksum to the exported data */
  4. addChecksum?: boolean;
  5. /** Whether to ensure the integrity of the data when importing it (unless the checksum property doesn't exist) */
  6. ensureIntegrity?: boolean;
  7. };
  8. /** Serialized data of a DataStore instance */
  9. export type SerializedDataStore = {
  10. /** The ID of the DataStore instance */
  11. id: string;
  12. /** The serialized data */
  13. data: string;
  14. /** The format version of the data */
  15. formatVersion: number;
  16. /** Whether the data is encoded */
  17. encoded: boolean;
  18. /** The checksum of the data - key is not present for data without a checksum */
  19. checksum?: string;
  20. };
  21. /**
  22. * Allows for easy serialization and deserialization of multiple DataStore instances.
  23. * Needs to run in a secure context (HTTPS) due to the use of the Web Crypto API.
  24. */
  25. export class DataStoreSerializer {
  26. protected stores: DataStore[];
  27. protected options: Required<DataStoreSerializerOptions>;
  28. constructor(stores: DataStore[], options: DataStoreSerializerOptions = {}) {
  29. if(!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
  30. throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
  31. this.stores = stores;
  32. this.options = {
  33. addChecksum: true,
  34. ensureIntegrity: true,
  35. ...options,
  36. };
  37. }
  38. /** Calculates the checksum of a string */
  39. protected async calcChecksum(input: string): Promise<string> {
  40. const encoder = new TextEncoder();
  41. const data = encoder.encode(input);
  42. const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  43. const hashArray = Array.from(new Uint8Array(hashBuffer));
  44. const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join("");
  45. return hashHex;
  46. }
  47. /** Serializes a DataStore instance */
  48. protected async serializeStore(store: DataStore): Promise<SerializedDataStore> {
  49. const data = store.encodingEnabled()
  50. ? await store.encodeData(JSON.stringify(store.getData()))
  51. : JSON.stringify(store.getData());
  52. const checksum = this.options.addChecksum
  53. ? await this.calcChecksum(data)
  54. : undefined;
  55. return {
  56. id: store.id,
  57. data,
  58. formatVersion: store.formatVersion,
  59. encoded: store.encodingEnabled(),
  60. checksum,
  61. };
  62. }
  63. /** Serializes the data stores into a string */
  64. public async serialize(): Promise<string> {
  65. const serData: SerializedDataStore[] = [];
  66. for(const store of this.stores)
  67. serData.push(await this.serializeStore(store));
  68. return JSON.stringify(serData);
  69. }
  70. /**
  71. * Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.
  72. * Also triggers the migration process if the data format has changed.
  73. */
  74. public async deserialize(serializedData: string): Promise<void> {
  75. const deserStores: SerializedDataStore[] = JSON.parse(serializedData);
  76. for(const storeData of deserStores) {
  77. const storeInst = this.stores.find(s => s.id === storeData.id);
  78. if(!storeInst)
  79. throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
  80. if(this.options.ensureIntegrity && typeof storeData.checksum === "string") {
  81. const checksum = await this.calcChecksum(storeData.data);
  82. if(checksum !== storeData.checksum)
  83. throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!\nExpected: ${storeData.checksum}\nHas: ${checksum}`);
  84. }
  85. const decodedData = storeData.encoded && storeInst.encodingEnabled()
  86. ? await storeInst.decodeData(storeData.data)
  87. : storeData.data;
  88. if(storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion) {
  89. console.log("[BetterYTM/#DEBUG] UU - running migrations");
  90. await storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
  91. }
  92. else {
  93. console.log("[BetterYTM/#DEBUG] UU - setting directly", JSON.parse(decodedData));
  94. await storeInst.setData(JSON.parse(decodedData));
  95. }
  96. }
  97. }
  98. }