DataStoreSerializer.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { getUnsafeWindow, computeHash, type DataStore } from "./index.js";
  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. *
  24. * 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.
  25. * Remember that you can call `super.methodName()` in the subclass to access the original method.
  26. *
  27. * ⚠️ Needs to run in a secure context (HTTPS) due to the use of the SubtleCrypto API if checksumming is enabled.
  28. */
  29. export class DataStoreSerializer {
  30. protected stores: DataStore[];
  31. protected options: Required<DataStoreSerializerOptions>;
  32. constructor(stores: DataStore[], options: DataStoreSerializerOptions = {}) {
  33. if(!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
  34. throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
  35. this.stores = stores;
  36. this.options = {
  37. addChecksum: true,
  38. ensureIntegrity: true,
  39. ...options,
  40. };
  41. }
  42. /** Calculates the checksum of a string */
  43. protected async calcChecksum(input: string): Promise<string> {
  44. return computeHash(input, "SHA-256");
  45. }
  46. /** Serializes a DataStore instance */
  47. protected async serializeStore(storeInst: DataStore): Promise<SerializedDataStore> {
  48. const data = storeInst.encodingEnabled()
  49. ? await storeInst.encodeData(JSON.stringify(storeInst.getData()))
  50. : JSON.stringify(storeInst.getData());
  51. const checksum = this.options.addChecksum
  52. ? await this.calcChecksum(data)
  53. : undefined;
  54. return {
  55. id: storeInst.id,
  56. data,
  57. formatVersion: storeInst.formatVersion,
  58. encoded: storeInst.encodingEnabled(),
  59. checksum,
  60. };
  61. }
  62. /** Serializes the data stores into a string */
  63. public async serialize(): Promise<string> {
  64. const serData: SerializedDataStore[] = [];
  65. for(const store of this.stores)
  66. serData.push(await this.serializeStore(store));
  67. return JSON.stringify(serData);
  68. }
  69. /**
  70. * Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.
  71. * Also triggers the migration process if the data format has changed.
  72. */
  73. public async deserialize(serializedData: string): Promise<void> {
  74. const deserStores: SerializedDataStore[] = JSON.parse(serializedData);
  75. for(const storeData of deserStores) {
  76. const storeInst = this.stores.find(s => s.id === storeData.id);
  77. if(!storeInst)
  78. throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
  79. if(this.options.ensureIntegrity && typeof storeData.checksum === "string") {
  80. const checksum = await this.calcChecksum(storeData.data);
  81. if(checksum !== storeData.checksum)
  82. throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!\nExpected: ${storeData.checksum}\nHas: ${checksum}`);
  83. }
  84. const decodedData = storeData.encoded && storeInst.encodingEnabled()
  85. ? await storeInst.decodeData(storeData.data)
  86. : storeData.data;
  87. if(storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
  88. await storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
  89. else
  90. await storeInst.setData(JSON.parse(decodedData));
  91. }
  92. }
  93. }